2025-04-20 18:20:44 +01:00
|
|
|
import { exec, spawn } from 'child_process';
|
|
|
|
import settings from '../../settings.js';
|
2025-04-21 08:06:19 +01:00
|
|
|
import { sendAudioRequest } from '../models/pollinations.js';
|
2025-02-12 16:26:48 +00:00
|
|
|
|
2025-02-12 19:10:14 +00:00
|
|
|
let speakingQueue = [];
|
|
|
|
let isSpeaking = false;
|
|
|
|
|
2025-04-20 18:20:44 +01:00
|
|
|
export function say(text) {
|
|
|
|
speakingQueue.push(text);
|
|
|
|
if (!isSpeaking) processQueue();
|
2025-02-12 19:10:14 +00:00
|
|
|
}
|
|
|
|
|
2025-04-20 18:20:44 +01:00
|
|
|
async function processQueue() {
|
2025-02-12 19:10:14 +00:00
|
|
|
if (speakingQueue.length === 0) {
|
|
|
|
isSpeaking = false;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
isSpeaking = true;
|
2025-04-20 18:20:44 +01:00
|
|
|
const txt = speakingQueue.shift();
|
|
|
|
|
|
|
|
const isWin = process.platform === 'win32';
|
|
|
|
const isMac = process.platform === 'darwin';
|
2025-04-21 08:06:19 +01:00
|
|
|
const model = settings.speak_model || 'pollinations/openai-audio/echo';
|
2025-02-12 16:26:48 +00:00
|
|
|
|
2025-04-20 18:20:44 +01:00
|
|
|
if (model === 'system') {
|
|
|
|
// system TTS
|
|
|
|
const cmd = isWin
|
|
|
|
? `powershell -NoProfile -Command "Add-Type -AssemblyName System.Speech; \
|
|
|
|
$s=New-Object System.Speech.Synthesis.SpeechSynthesizer; $s.Rate=2; \
|
|
|
|
$s.Speak('${txt.replace(/'/g,"''")}'); $s.Dispose()"`
|
|
|
|
: isMac
|
|
|
|
? `say "${txt.replace(/"/g,'\\"')}"`
|
|
|
|
: `espeak "${txt.replace(/"/g,'\\"')}"`;
|
|
|
|
|
|
|
|
exec(cmd, err => {
|
|
|
|
if (err) console.error('TTS error', err);
|
|
|
|
processQueue();
|
|
|
|
});
|
2025-02-12 16:26:48 +00:00
|
|
|
|
|
|
|
} else {
|
2025-04-20 18:20:44 +01:00
|
|
|
// remote audio provider
|
|
|
|
const [prov, mdl, voice] = model.split('/');
|
|
|
|
if (prov !== 'pollinations') throw new Error(`Unknown provider: ${prov}`);
|
2025-02-12 16:26:48 +00:00
|
|
|
|
2025-04-20 18:20:44 +01:00
|
|
|
try {
|
2025-04-21 08:06:19 +01:00
|
|
|
let audioData = await sendAudioRequest(txt, mdl, voice);
|
|
|
|
if (!audioData) {
|
|
|
|
audioData = "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU5LjI3LjEwMAAAAAAAAAAAAAAA/+NAwAAAAAAAAAAAAEluZm8AAAAPAAAAAAAAANAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAExhdmM1OS4zNwAAAAAAAAAAAAAAAAAAAAAAAAAAAADQAAAeowAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
|
|
|
|
// ^ 0 second silent audio clip
|
|
|
|
}
|
2025-04-20 18:20:44 +01:00
|
|
|
|
|
|
|
if (isWin) {
|
|
|
|
const ps = `
|
|
|
|
Add-Type -AssemblyName presentationCore;
|
|
|
|
$p=New-Object System.Windows.Media.MediaPlayer;
|
|
|
|
$p.Open([Uri]::new("data:audio/mp3;base64,${audioData}"));
|
|
|
|
$p.Play();
|
|
|
|
Start-Sleep -Seconds [math]::Ceiling($p.NaturalDuration.TimeSpan.TotalSeconds);
|
|
|
|
`;
|
|
|
|
spawn('powershell', ['-NoProfile','-Command', ps], {
|
|
|
|
stdio: 'ignore', detached: true
|
|
|
|
}).unref();
|
|
|
|
processQueue();
|
|
|
|
|
|
|
|
} else {
|
|
|
|
const player = spawn('ffplay', ['-nodisp','-autoexit','pipe:0'], {
|
|
|
|
stdio: ['pipe','ignore','ignore']
|
|
|
|
});
|
|
|
|
player.stdin.write(Buffer.from(audioData, 'base64'));
|
|
|
|
player.stdin.end();
|
|
|
|
player.on('exit', processQueue);
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
console.error('Audio error', e);
|
|
|
|
processQueue();
|
2025-03-13 14:40:18 -05:00
|
|
|
}
|
2025-04-20 18:20:44 +01:00
|
|
|
}
|
2025-02-12 16:26:48 +00:00
|
|
|
}
|