84 lines
2.5 KiB
TypeScript
84 lines
2.5 KiB
TypeScript
import { spawn } from 'child_process';
|
|
import path from 'path';
|
|
import fs from 'fs';
|
|
import os from 'os';
|
|
import { getConfig } from '../config/config.js';
|
|
|
|
interface TTSConfig {
|
|
piperPath: string;
|
|
modelPath: string;
|
|
configPath?: string;
|
|
ffmpegPath: string;
|
|
tempDir: string;
|
|
}
|
|
|
|
function getTTSConfig(): TTSConfig | null {
|
|
const cfg = getConfig().getConfig() as any;
|
|
const voice = cfg?.voice;
|
|
if (!voice?.tts) return null;
|
|
|
|
return {
|
|
piperPath: voice.tts.piperPath || 'piper',
|
|
modelPath: voice.tts.modelPath || process.env.PIPER_MODEL_PATH || '',
|
|
configPath: voice.tts.configPath || process.env.PIPER_CONFIG_PATH,
|
|
ffmpegPath: voice.ffmpegPath || 'ffmpeg',
|
|
tempDir: voice.tempDir || os.tmpdir(),
|
|
};
|
|
}
|
|
|
|
export function isTTSAvailable(): boolean {
|
|
return getTTSConfig() !== null;
|
|
}
|
|
|
|
export async function synthesizeSpeech(text: string): Promise<Buffer> {
|
|
const config = getTTSConfig();
|
|
if (!config) throw new Error('TTS not configured');
|
|
|
|
const truncatedText = text.slice(0, 4000);
|
|
const tempDir = config.tempDir;
|
|
const wavPath = path.join(tempDir, `tts_output_${Date.now()}.wav`);
|
|
const oggPath = path.join(tempDir, `tts_output_${Date.now()}.ogg`);
|
|
|
|
try {
|
|
// Generate WAV via Piper
|
|
const piperArgs = ['--model', config.modelPath, '--output_file', wavPath];
|
|
if (config.configPath) {
|
|
piperArgs.push('--config', config.configPath);
|
|
}
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
const piper = spawn(config.piperPath, piperArgs);
|
|
let stderr = '';
|
|
piper.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
|
|
piper.stdin.write(truncatedText);
|
|
piper.stdin.end();
|
|
piper.on('close', (code) => {
|
|
if (code === 0) resolve();
|
|
else reject(new Error(`Piper exited ${code}: ${stderr}`));
|
|
});
|
|
piper.on('error', reject);
|
|
});
|
|
|
|
// Convert WAV to OGG/Opus for Telegram sendVoice
|
|
await new Promise<void>((resolve, reject) => {
|
|
const ffmpeg = spawn(config.ffmpegPath, [
|
|
'-y', '-i', wavPath,
|
|
'-c:a', 'libopus', '-b:a', '48k', '-vbr', 'on',
|
|
'-compression_level', '10',
|
|
oggPath,
|
|
]);
|
|
let stderr = '';
|
|
ffmpeg.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
|
|
ffmpeg.on('close', (code) => {
|
|
if (code === 0) resolve();
|
|
else reject(new Error(`ffmpeg exited ${code}: ${stderr}`));
|
|
});
|
|
ffmpeg.on('error', reject);
|
|
});
|
|
|
|
return fs.readFileSync(oggPath);
|
|
} finally {
|
|
try { fs.unlinkSync(wavPath); } catch {}
|
|
try { fs.unlinkSync(oggPath); } catch {}
|
|
}
|
|
} |