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
+13 -1
View File
@@ -2027,6 +2027,10 @@ function fillProfileControls() {
$("#tts-echo-value").textContent = Number($("#tts-echo").value) > 0 ? $("#tts-echo").value : "off";
$("#tts-words").checked = state.speech.words === true;
$("#tts-vox").checked = state.speech.vox === true;
$("#tts-vox-clean").checked = state.speech.voxClean === true;
$("#tts-vox-clean").disabled = state.speech.vox !== true;
$("#tts-words").disabled = state.speech.vox === true;
const gap = Number(state.speech.wordGapMs);
$("#tts-word-gap").value = Number.isFinite(gap) && state.speech.wordGapMs !== null ? gap : 120;
$("#tts-word-gap-value").textContent = `${$("#tts-word-gap").value}ms`;
@@ -2058,7 +2062,9 @@ function effectOptions() {
highpass: Number($("#tts-highpass").value),
echo: Number($("#tts-echo").value),
words: $("#tts-words").checked,
wordGapMs: Number($("#tts-word-gap").value)
wordGapMs: Number($("#tts-word-gap").value),
vox: $("#tts-vox").checked,
voxClean: $("#tts-vox-clean").checked
};
}
function syncVoiceControls(voices, selected) {
@@ -2734,6 +2740,12 @@ function init() {
$(`#${id}`).addEventListener("change", () => void saveSpeech());
}
$("#tts-words").addEventListener("change", () => void saveSpeech());
$("#tts-vox").addEventListener("change", () => {
$("#tts-vox-clean").disabled = !$("#tts-vox").checked;
$("#tts-words").disabled = $("#tts-vox").checked;
void saveSpeech();
});
$("#tts-vox-clean").addEventListener("change", () => void saveSpeech());
$("#profile-open").addEventListener("click", () => {
const panel = document.querySelector(".vk-profile-panel");
if (panel) {
+13
View File
@@ -556,6 +556,19 @@
<input id="tts-word-gap" type="range" min="0" max="500" step="10" value="120" />
<span id="tts-word-gap-value" class="vk-mono vk-dim">120ms</span>
</div>
<div class="vk-profile-row">
<label class="vk-tts-check">
<input id="tts-vox" type="checkbox" /> VOX prosody
<span
class="vk-info"
title="NetTTS-style Black Mesa delivery for the Flextalk voices: sentence breaks, lead-ins, weighted pauses, and vendor tags. Needs python3. Overrides word mode."
>i</span
>
</label>
<label class="vk-tts-check">
<input id="tts-vox-clean" type="checkbox" /> clean (no stutter)
</label>
</div>
</section>
<section class="vk-profile-section">
<div class="vk-section-head">
+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",