mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-25 04:13:51 +08:00
fix(voice): harden timeout parsing, tts input check, and player abort
- fall back to the default when VOICE_TIMEOUT_MS is non-numeric or <= 0, so a bad override can't make the abort fire immediately - type-check the tts `text` before calling .trim() so a non-string body returns 400 instead of throwing - abort the in-flight TTS fetch on stop() and on a superseding play, so tapping read-aloud repeatedly doesn't leave orphaned requests generating audio
This commit is contained in:
@@ -39,7 +39,12 @@ function resolveConfig(req) {
|
||||
const router = express.Router();
|
||||
|
||||
// Generous by default — local TTS can synthesize long messages at ~real-time on CPU.
|
||||
const VOICE_TIMEOUT_MS = Number(process.env.VOICE_TIMEOUT_MS || 300000);
|
||||
// Guard against a non-numeric/zero override that would make setTimeout fire immediately.
|
||||
const DEFAULT_VOICE_TIMEOUT_MS = 300000;
|
||||
const _parsedTimeout = Number(process.env.VOICE_TIMEOUT_MS);
|
||||
const VOICE_TIMEOUT_MS = Number.isFinite(_parsedTimeout) && _parsedTimeout > 0
|
||||
? _parsedTimeout
|
||||
: DEFAULT_VOICE_TIMEOUT_MS;
|
||||
|
||||
/**
|
||||
* fetch() with an AbortController timeout so a stalled backend can't hold the
|
||||
@@ -183,7 +188,7 @@ router.post('/tts', async (req, res) => {
|
||||
if (!cfg.baseUrl) return res.status(503).json({ error: 'No voice backend configured' });
|
||||
if (!isAllowedBackendUrl(cfg.baseUrl)) return res.status(400).json({ error: 'Invalid voice backend URL.' });
|
||||
const text = req.body?.text;
|
||||
if (!text || !text.trim()) return res.status(400).json({ error: 'text required' });
|
||||
if (typeof text !== 'string' || !text.trim()) return res.status(400).json({ error: 'text required' });
|
||||
try {
|
||||
const r = await fetchWithTimeout(`${cfg.baseUrl}/audio/speech`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -30,7 +30,8 @@ class VoicePlayer {
|
||||
private state: VoicePlayState = 'idle';
|
||||
private errorId: string | null = null;
|
||||
private errorMsg: string | null = null;
|
||||
private token = 0; // bumps to cancel in-flight fetches
|
||||
private token = 0; // bumps to ignore stale in-flight results
|
||||
private activeController: AbortController | null = null; // aborts the in-flight TTS fetch
|
||||
private errorTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private listeners = new Set<() => void>();
|
||||
|
||||
@@ -89,13 +90,21 @@ class VoicePlayer {
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.token++; // cancel any in-flight fetch
|
||||
this.token++; // ignore any stale in-flight result
|
||||
this.abortActive(); // and actually cancel the network request
|
||||
if (this.audio) this.audio.pause();
|
||||
this.state = 'idle';
|
||||
this.currentId = null;
|
||||
this.emit();
|
||||
}
|
||||
|
||||
private abortActive() {
|
||||
if (this.activeController) {
|
||||
this.activeController.abort();
|
||||
this.activeController = null;
|
||||
}
|
||||
}
|
||||
|
||||
private onEnded() {
|
||||
this.state = 'idle';
|
||||
this.currentId = null;
|
||||
@@ -130,18 +139,23 @@ class VoicePlayer {
|
||||
this.emit();
|
||||
|
||||
const myToken = ++this.token;
|
||||
this.abortActive(); // cancel any request this play supersedes
|
||||
|
||||
try {
|
||||
let url = this.cache.get(id);
|
||||
if (!url) {
|
||||
const controller = new AbortController();
|
||||
this.activeController = controller;
|
||||
const timer = setTimeout(() => controller.abort(), CLIENT_TIMEOUT_MS);
|
||||
const res = await authenticatedFetch('/api/voice/tts', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ text: content }),
|
||||
headers: voiceConfigHeaders(),
|
||||
signal: controller.signal,
|
||||
}).finally(() => clearTimeout(timer));
|
||||
}).finally(() => {
|
||||
clearTimeout(timer);
|
||||
if (this.activeController === controller) this.activeController = null;
|
||||
});
|
||||
if (myToken !== this.token) return; // superseded by another play/stop
|
||||
if (!res.ok) {
|
||||
let msg = `Read-aloud failed (${res.status})`;
|
||||
|
||||
Reference in New Issue
Block a user