Merge pull request 'fix(dankvoice): pin thinking off in rewrite, fix silent and timeout modes' (#33) from fix/32-dankvoice-latency into main
This commit was merged in pull request #33.
This commit is contained in:
@@ -576,7 +576,18 @@ export function resolveRewriter(env: NodeJS.ProcessEnv, modelsYml: string, confi
|
||||
return resolveTinyRewriter(modelsYml, configYml);
|
||||
}
|
||||
|
||||
/** The enhancer's rewrite step; never throws — null is its "couldn't". */
|
||||
/**
|
||||
* The enhancer's rewrite step; never throws — null is its "couldn't".
|
||||
*
|
||||
* The `chat_template_kwargs.enable_thinking: false` field is load-bearing.
|
||||
* The @tiny fleet model (frank-zealot, a Qwen35 hybrid) defaults to a
|
||||
* reasoning pass that fills `reasoning_content` and leaves `content` empty.
|
||||
* The harness injects this flag for the built-in vocalizer; a raw POST does
|
||||
* not get it, so without it the rewrite spends its whole 6 s budget thinking
|
||||
* (blowing the timeout → mechanical fallback) and, when it answers in time,
|
||||
* returns an empty `content` that would mute the utterance. Measured: 6.9 s /
|
||||
* empty content without the flag, 0.55 s / clean prose with it.
|
||||
*/
|
||||
export async function rewriteForSpeech(target: RewriteTarget, text: string): Promise<string | null> {
|
||||
try {
|
||||
const res = await fetch(target.chatUrl, {
|
||||
@@ -589,6 +600,7 @@ export async function rewriteForSpeech(target: RewriteTarget, text: string): Pro
|
||||
{ role: "user", content: text.slice(0, REWRITE_MAX_INPUT) },
|
||||
],
|
||||
max_tokens: REWRITE_MAX_TOKENS,
|
||||
chat_template_kwargs: { enable_thinking: false },
|
||||
}),
|
||||
signal: AbortSignal.timeout(REWRITE_TIMEOUT_MS),
|
||||
});
|
||||
@@ -600,20 +612,33 @@ export async function rewriteForSpeech(target: RewriteTarget, text: string): Pro
|
||||
const message: unknown = first.message;
|
||||
if (!message || typeof message !== "object" || !("content" in message)) return null;
|
||||
const content: unknown = message.content;
|
||||
if (typeof content === "string") return content.trim();
|
||||
if (!Array.isArray(content)) return null;
|
||||
const parts: string[] = [];
|
||||
for (const part of content) {
|
||||
if (part && typeof part === "object" && "type" in part && part.type === "text" && "text" in part && typeof part.text === "string") {
|
||||
parts.push(part.text);
|
||||
}
|
||||
const spoken = typeof content === "string" ? content.trim() : Array.isArray(content) ? textParts(content) : null;
|
||||
if (spoken === null) return null;
|
||||
if (spoken === "") {
|
||||
// A reasoning-only reply means the endpoint ignored the thinking-off
|
||||
// flag: the prose landed in reasoning_content, not content. That is a
|
||||
// failed rewrite (fall back to mechanical text), NOT the enhancer's
|
||||
// legitimate "" ("nothing worth saying") — which has no reasoning.
|
||||
const reasoning = "reasoning_content" in message ? message.reasoning_content : undefined;
|
||||
if (typeof reasoning === "string" && reasoning.trim().length > 0) return null;
|
||||
}
|
||||
return parts.join(" ").trim();
|
||||
return spoken;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Concatenated text of a content-part array; undefined shape handled by caller. */
|
||||
function textParts(content: readonly unknown[]): string {
|
||||
const parts: string[] = [];
|
||||
for (const part of content) {
|
||||
if (part && typeof part === "object" && "type" in part && part.type === "text" && "text" in part && typeof part.text === "string") {
|
||||
parts.push(part.text);
|
||||
}
|
||||
}
|
||||
return parts.join(" ").trim();
|
||||
}
|
||||
|
||||
/** The text the assistant last actually said; tool-only messages do not count. */
|
||||
export function lastAssistantText(messages: readonly unknown[]): string {
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
@@ -1043,7 +1068,14 @@ export default function dankvoice(pi: ExtensionAPI) {
|
||||
persistSettings(ctx);
|
||||
return;
|
||||
}
|
||||
await speak(ctx, `dankvoice: ${voiceLabel()} through ${cfg.player?.command ?? "no player"}, checking.`);
|
||||
// Fire-and-forget on purpose: awaiting the playback chain here blocks
|
||||
// the menu handler for the whole phrase and the harness reports the
|
||||
// command as timed out. The point of the test is to hear it, not to
|
||||
// block the menu on it.
|
||||
ctx.ui.notify("dankvoice: speaking the test phrase", "info");
|
||||
void speak(ctx, `dankvoice: ${voiceLabel()} through ${cfg.player?.command ?? "no player"}, checking.`).catch((error) =>
|
||||
ctx.ui.notify(`dankvoice: ${error instanceof Error ? error.message : String(error)}`, "error"),
|
||||
);
|
||||
}
|
||||
|
||||
pi.registerCommand("say", {
|
||||
|
||||
Reference in New Issue
Block a user