| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110 |
- // scripts/tts.mjs — 口播配音(provider: tts)
- //
- // ⚠️ 现状:平台暂无 TTS 通道。
- // 但本地 edge-tts(若已安装)可直接使用 —— probe 会优先探测本地。
- // 两条路都不可用时,明确抛出 ProviderUnavailableError + 三条替代方案。
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
- import { join } from 'node:path';
- import {
- parseArgs, readJSON, writeJSON, paths, ensureWork, log,
- } from '../lib/index.mjs';
- import { getProvider } from '../lib/providers.mjs';
- import { probe as ffprobe } from '../lib/ffmpeg.mjs';
- export async function synthesize(text, { outPath, voice } = {}) {
- const p = getProvider('tts');
- const dir = paths.audio;
- mkdirSync(dir, { recursive: true });
- const out = outPath || join(dir, 'voiceover.mp3');
- const r = await p.call({ text, outPath: out, voice });
- return r;
- }
- export async function main(argv = process.argv.slice(2)) {
- const { _, flags } = parseArgs(argv);
- if (flags.help || (!flags.text && !flags.from && !flags.probe)) {
- console.log(`
- fmode-mv tts — 口播配音(tts 通道)
- 用法:
- fmode-mv tts --probe # 探活(含本地 edge-tts 探测)
- fmode-mv tts --text "大家好,今天讲三件事。" --out work/audio/voiceover.mp3
- fmode-mv tts --from work/script.md # 整个口播稿合成
- 选项:
- --text "..." 直接给文本
- --from <file> 口播稿文件(纯文本/md)
- --voice <name> 音色(默认 zh-CN-XiaoxiaoNeural,edge-tts 音色名)
- --out <file> 输出文件(默认 work/audio/voiceover.mp3)
- --probe 只探活
- ⚠️ 现状:平台暂无 TTS 通道。替代方案:
- A) 本地 edge-tts(推荐,免费):pip install edge-tts 安装后本命令自动使用
- B) 对接 OpenAI 兼容 /v1/audio/speech:
- export FEME_TTS_ENDPOINT=https://your-host/v1/audio/speech
- C) 自备配音音频放到 work/audio/voiceover.mp3,用 --from 跳过本步
- `);
- return 0;
- }
- const p = getProvider('tts');
- const pr = await p.probe({});
- if (flags.probe) {
- log.step('TTS 通道探活');
- log.key('状态', pr.available ? '可用' : '不可用');
- log.key('提供方', pr.provider || '平台网关');
- log.info(pr.detail);
- return pr.available ? 0 : 3;
- }
- if (!pr.available) {
- throw Object.assign(new Error(`TTS 通道不可用:${pr.detail}`), { hint: p.hint, name: 'ProviderUnavailableError' });
- }
- ensureWork();
- let text = flags.text ? String(flags.text) : null;
- if (!text && flags.from) {
- if (!existsSync(String(flags.from))) throw new Error(`文件不存在:${flags.from}`);
- text = readFileSync(String(flags.from), 'utf8')
- .replace(/^#{1,6}\s+/gm, '')
- .replace(/[*_`>]/g, '')
- .trim();
- }
- if (!text) throw new Error('缺少文本:用 --text 或 --from');
- log.step(`合成配音(${pr.provider || '网关'})`);
- log.key('字数', `${text.length} 字`);
- log.key('音色', flags.voice || p.defaultVoice());
- const out = typeof flags.out === 'string' ? flags.out : join(paths.audio, 'voiceover.mp3');
- const r = await synthesize(text, { outPath: out, voice: flags.voice });
- const info = await ffprobe(r.outPath).catch(() => null);
- log.ok(`→ ${r.outPath}${info ? `(${info.duration.toFixed(1)}s)` : ''}`);
- writeJSON(join(paths.audio, 'voiceover.json'), {
- text,
- voice: r.voice,
- provider: r.provider,
- path: r.outPath,
- duration: info?.duration ?? null,
- _meta: { createdBy: 'fmode-mv/tts', generatedAt: new Date().toISOString() },
- });
- return 0;
- }
- if (import.meta.url === `file://${process.argv[1]}`) {
- main().then((c) => process.exit(Number(c) || 0)).catch(async (e) => {
- const { reportUnavailable, log: L } = await import('../lib/index.mjs');
- if (e?.name === 'ProviderUnavailableError' || e?.hint) {
- reportUnavailable('tts', e);
- process.exit(3);
- }
- L.err(e.message);
- process.exit(1);
- });
- }
- export { readJSON, writeFileSync };
|