Add the VOX prosody preference and panel controls

This commit is contained in:
Fisto
2026-09-21 11:08:22 +02:00
parent ca3fa5be1e
commit 293d131d2b
9 changed files with 101 additions and 17 deletions
+21 -3
View File
@@ -561,10 +561,26 @@ function buildServer(): McpServer {
highpass: z.number().min(40).max(400).optional().describe("High-pass filter Hz"),
echo: z.number().min(0).max(100).optional().describe("Mono echo amount"),
words: z.boolean().optional().describe("Synthesize each word and merge with gaps"),
wordGapMs: z.number().min(0).max(500).optional().describe("Silence between words in ms")
wordGapMs: z.number().min(0).max(500).optional().describe("Silence between words in ms"),
vox: z.boolean().optional().describe("NetTTS-style VOX prosody (Flextalk voices)"),
voxClean: z.boolean().optional().describe("VOX without the staccato wrapper")
}
},
async ({ text, provider, voice, speed, pitch, expression, profile, highpass, echo, words, wordGapMs }) =>
async ({
text,
provider,
voice,
speed,
pitch,
expression,
profile,
highpass,
echo,
words,
wordGapMs,
vox,
voxClean
}) =>
jsonText(
await audio.speak(text, {
provider,
@@ -576,7 +592,9 @@ function buildServer(): McpServer {
highpass,
echo,
words,
wordGapMs
wordGapMs,
vox,
voxClean
})
)
);
+13 -3
View File
@@ -30,6 +30,10 @@ export interface TtsPreferences {
words: boolean;
/** Silence between words in milliseconds, 0-500. */
wordGapMs: number | null;
/** NetTTS-style VOX prosody (Flextalk voices only). */
vox: boolean;
/** VOX without the staccato wrapper. */
voxClean: boolean;
}
const PROFILE_RANGES: Record<string, [number, number]> = {
@@ -146,7 +150,9 @@ const DEFAULTS: WebPreferences = {
highpass: null,
echo: null,
words: false,
wordGapMs: null
wordGapMs: null,
vox: false,
voxClean: false
},
audio: { duck: true },
favourites: { users: [], channels: [] },
@@ -340,7 +346,9 @@ export class PreferencesStore {
highpass: normalizeRange(patch.tts?.highpass, 40, 400),
echo: normalizeRange(patch.tts?.echo, 0, 100),
words: patch.tts?.words === true,
wordGapMs: normalizeRange(patch.tts?.wordGapMs, 0, 500)
wordGapMs: normalizeRange(patch.tts?.wordGapMs, 0, 500),
vox: patch.tts?.vox === true,
voxClean: patch.tts?.voxClean === true
};
}
@@ -405,7 +413,9 @@ export class PreferencesStore {
highpass: normalizeRange(raw?.tts?.highpass, 40, 400),
echo: normalizeRange(raw?.tts?.echo, 0, 100),
words: raw?.tts?.words === true,
wordGapMs: normalizeRange(raw?.tts?.wordGapMs, 0, 500)
wordGapMs: normalizeRange(raw?.tts?.wordGapMs, 0, 500),
vox: raw?.tts?.vox === true,
voxClean: raw?.tts?.voxClean === true
},
audio: {
duck: raw?.audio?.duck !== false
+9 -1
View File
@@ -34,6 +34,10 @@ export interface TtsOptions {
words?: boolean;
/** Silence between words in milliseconds, 0 to 500. */
wordGapMs?: number;
/** NetTTS-style VOX prosody (Flextalk voices only). */
vox?: boolean;
/** VOX without the staccato wrapper. */
voxClean?: boolean;
}
export interface TtsEngine {
@@ -348,6 +352,8 @@ export class Tts {
.replace("{echo}", options.echo !== undefined ? String(options.echo) : "")
.replace("{words}", options.words ? "1" : "")
.replace("{word_gap_ms}", options.wordGapMs !== undefined ? String(options.wordGapMs) : "")
.replace("{vox}", options.vox ? "1" : "")
.replace("{vox_clean}", options.voxClean ? "1" : "")
)
: ["espeak-ng", ...(options.voice ? ["-v", options.voice] : []), "-w", out, "-f", textFile];
@@ -371,7 +377,9 @@ export class Tts {
...(options.words ? { VESKSLOP_TTS_WORDS: "1" } : {}),
...(options.wordGapMs !== undefined
? { VESKSLOP_TTS_WORD_GAP_MS: String(options.wordGapMs) }
: {})
: {}),
...(options.vox ? { VESKSLOP_TTS_VOX: "1" } : {}),
...(options.voxClean ? { VESKSLOP_TTS_VOX_CLEAN: "1" } : {})
},
stdio: ["ignore", "ignore", "pipe"]
});
+8 -2
View File
@@ -508,7 +508,9 @@ export function startWebUi(deps: WebUiDeps): WebUiHandle {
highpass: tts?.highpass ?? null,
echo: tts?.echo ?? null,
words: tts?.words === true,
wordGapMs: tts?.wordGapMs ?? null
wordGapMs: tts?.wordGapMs ?? null,
vox: tts?.vox === true,
voxClean: tts?.voxClean === true
};
}
@@ -756,6 +758,8 @@ export function startWebUi(deps: WebUiDeps): WebUiHandle {
const words = req.body?.words !== undefined ? req.body.words === true : prefs.tts.words;
const wordGapMs =
req.body?.wordGapMs !== undefined ? Number(req.body.wordGapMs) : prefs.tts.wordGapMs ?? undefined;
const vox = req.body?.vox !== undefined ? req.body.vox === true : prefs.tts.vox;
const voxClean = req.body?.voxClean !== undefined ? req.body.voxClean === true : prefs.tts.voxClean;
const result = await deps.audio.speak(text, {
provider: provider ?? undefined,
@@ -767,7 +771,9 @@ export function startWebUi(deps: WebUiDeps): WebUiHandle {
...(Number.isFinite(highpass) ? { highpass } : {}),
...(Number.isFinite(echo) ? { echo } : {}),
...(words ? { words: true } : {}),
...(Number.isFinite(wordGapMs) ? { wordGapMs } : {})
...(Number.isFinite(wordGapMs) ? { wordGapMs } : {}),
...(vox ? { vox: true } : {}),
...(voxClean ? { voxClean: true } : {})
});
deps.getPuppet()?.appendAction({
type: "audio",