|
|
@@ -18,12 +18,35 @@
|
|
|
* zh-CN-YunjianNeural — 男声,老成
|
|
|
*/
|
|
|
|
|
|
-import { spawn } from 'child_process';
|
|
|
+import { spawn, execSync } from 'child_process';
|
|
|
import fs from 'fs';
|
|
|
import path from 'path';
|
|
|
import { VoiceParams } from '../../types';
|
|
|
import { ITtsProvider } from './provider.interface';
|
|
|
|
|
|
+/** 解析 edge-tts 可执行文件的完整路径(带缓存) */
|
|
|
+let _edgeTtsBin: string | null = null;
|
|
|
+function getEdgeTtsBin(): string {
|
|
|
+ if (_edgeTtsBin) return _edgeTtsBin;
|
|
|
+ try {
|
|
|
+ // Windows: where edge-tts, Unix: which edge-tts
|
|
|
+ const cmd = process.platform === 'win32' ? 'where edge-tts' : 'which edge-tts';
|
|
|
+ const result = execSync(cmd, { encoding: 'utf8', timeout: 5000 }).trim();
|
|
|
+ const lines = result.split('\n').filter(l => l.trim());
|
|
|
+ // 优先选 venv 里的(不在 hermes-agent 的 venv 里选,因为那个可能有 PATH 问题)
|
|
|
+ // 但实际上取第一个可用的就行
|
|
|
+ _edgeTtsBin = lines[0]?.trim();
|
|
|
+ if (_edgeTtsBin) {
|
|
|
+ console.log(`🔍 [EdgeTTS] 解析路径: ${_edgeTtsBin}`);
|
|
|
+ return _edgeTtsBin;
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ console.warn('⚠️ [EdgeTTS] 无法解析 edge-tts 路径,回退到 "edge-tts"');
|
|
|
+ }
|
|
|
+ _edgeTtsBin = 'edge-tts'; // fallback
|
|
|
+ return _edgeTtsBin;
|
|
|
+}
|
|
|
+
|
|
|
// Edge-TTS 中文音色映射表(10个统一音色 + 附加音色)
|
|
|
const EDGE_VOICE_MAP: Record<string, string> = {
|
|
|
// 统一 Voice ID → Edge-TTS 音色名(与 tts.service.ts 保持一致)
|
|
|
@@ -121,15 +144,17 @@ export class EdgeTtsProvider implements ITtsProvider {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- console.log(`📢 [EdgeTTS] 开始合成: voice=${voice}, text=${text.length}字, output=${path.basename(finalPath)}`);
|
|
|
+ const edgeTtsBin = getEdgeTtsBin();
|
|
|
+ console.log(`📢 [EdgeTTS] 开始合成: voice=${voice}, text=${text.length}字, output=${path.basename(finalPath)}, bin=${edgeTtsBin}`);
|
|
|
|
|
|
const startTime = Date.now();
|
|
|
|
|
|
return new Promise<string>((resolve, reject) => {
|
|
|
- // 优先用 edge-tts 命令(避免 python -m 解析长文本的 argparse 问题)
|
|
|
- // shell: false 避免 shell 解析带负号的参数(如 -20%)
|
|
|
- const edgeCmd = spawn('edge-tts', args, {
|
|
|
- shell: false,
|
|
|
+ // 修复 #tts-edge-shell: 不要用 shell:true,因为 --write-media 的输出路径
|
|
|
+ // 可能包含 ()&空格 等特殊字符,会被 /bin/sh 解析报错
|
|
|
+ // (语法错误: syntax error near unexpected token `(')
|
|
|
+ // getEdgeTtsBin() 已经返回了完整路径,无需依赖 PATH 查找
|
|
|
+ const edgeCmd = spawn(edgeTtsBin, args, {
|
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
|
});
|
|
|
|
|
|
@@ -143,9 +168,8 @@ export class EdgeTtsProvider implements ITtsProvider {
|
|
|
edgeCmd.on('error', (err) => {
|
|
|
if (exited) return;
|
|
|
exited = true;
|
|
|
- // 尝试 fallback:直接用 edge-tts 命令
|
|
|
- console.log(`⚠️ [EdgeTTS] python -m edge_tts 失败,尝试 edge-tts 命令: ${err.message}`);
|
|
|
- this.runEdgeTtsDirect(args, finalPath, startTime, resolve, reject);
|
|
|
+ console.log(`⚠️ [EdgeTTS] ${edgeTtsBin} 失败,尝试 python -m: ${err.message}`);
|
|
|
+ this.runEdgeTtsFallback(args, finalPath, startTime, resolve, reject);
|
|
|
});
|
|
|
|
|
|
edgeCmd.on('close', (code) => {
|
|
|
@@ -158,28 +182,23 @@ export class EdgeTtsProvider implements ITtsProvider {
|
|
|
console.log(`✅ [EdgeTTS] 完成: ${finalPath} (${stats.size} bytes, ${elapsed}ms)`);
|
|
|
resolve(finalPath);
|
|
|
} else {
|
|
|
- // 如果 python -m 失败,尝试 edge-tts 命令
|
|
|
- if (stderr.toLowerCase().includes('no module') || code !== 0) {
|
|
|
- console.log(`⚠️ [EdgeTTS] python -m 返回 code=${code},尝试 edge-tts 命令`);
|
|
|
- this.runEdgeTtsDirect(args, finalPath, startTime, resolve, reject);
|
|
|
- } else {
|
|
|
- reject(new Error(`EdgeTTS 合成失败 (code=${code}): ${stderr.substring(0, 200)}`));
|
|
|
- }
|
|
|
+ // stderr 不包含 'no module' 但有 traceback → 用 python -m 重试
|
|
|
+ console.log(`⚠️ [EdgeTTS] ${edgeTtsBin} 返回 code=${code},尝试 python -m edge_tts`);
|
|
|
+ this.runEdgeTtsFallback(args, finalPath, startTime, resolve, reject);
|
|
|
}
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- /** Fallback:直接用 edge-tts 命令 */
|
|
|
- private runEdgeTtsDirect(
|
|
|
+ /** Fallback: python -m edge_tts(当 edge-tts.exe 环境异常时使用) */
|
|
|
+ private runEdgeTtsFallback(
|
|
|
args: string[],
|
|
|
finalPath: string,
|
|
|
startTime: number,
|
|
|
resolve: (value: string) => void,
|
|
|
reject: (error: Error) => void,
|
|
|
) {
|
|
|
- const child = spawn('edge-tts', args, {
|
|
|
- shell: false,
|
|
|
+ const child = spawn('python', ['-m', 'edge_tts', ...args], {
|
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
|
});
|
|
|
|
|
|
@@ -209,9 +228,9 @@ export class EdgeTtsProvider implements ITtsProvider {
|
|
|
|
|
|
/** 健康检查:edge-tts 命令是否可用 */
|
|
|
async healthCheck(): Promise<boolean> {
|
|
|
+ const bin = getEdgeTtsBin();
|
|
|
return new Promise((resolve) => {
|
|
|
- const child = spawn('edge-tts', ['--list-voices'], {
|
|
|
- shell: false,
|
|
|
+ const child = spawn(bin, ['--list-voices'], {
|
|
|
stdio: 'pipe',
|
|
|
});
|
|
|
|