Let admins hear a voice sample before picking it

The Speech section of every model settings form now has "Hear a
sample", which streams a short sentence (AppData.VoicePreviewText) in
the form's model and voice before they're saved, through
GET api/admin/harness/preview-voice (super admins and admins, that
model only via grabModelCandidate). pickVoice is shared with
speakText, and explainAudioError reads why a streamed player failed
for both read-aloud and the sample.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-19 21:32:49 +01:00
co-authored by Claude Opus 5.5
parent 148d02dcfe
commit bc499a7466
10 changed files with 278 additions and 29 deletions
@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from "react";
import explainAudioError from "@/src/functions/frontend/explain-audio-error";
export type SpeakMessageState = "idle" | "loading" | "playing";
@@ -78,7 +79,7 @@ export default function useSpeakMessage({ enabled }: { enabled: boolean }): Spea
if (!isCurrent()) return;
reset();
explainError({ src }).then((text) => {
explainAudioError({ src, fallback: "Couldn't read it aloud. Try again." }).then((text) => {
setError({ message_id, text });
});
};
@@ -97,27 +98,3 @@ export default function useSpeakMessage({ enabled }: { enabled: boolean }): Spea
return { enabled, message_id, state, error, toggle };
}
/**
* The player only says it failed, so ask again for the reason: a failure
* answers in JSON, and audio coming back instead is dropped at once.
*/
async function explainError({ src }: { src: string }) {
const fallback = "Couldn't read it aloud. Try again.";
const controller = new AbortController();
try {
const res = await fetch(src, { signal: controller.signal, cache: "no-store" });
if (!res.headers.get("content-type")?.includes("json")) {
controller.abort();
return fallback;
}
const json = await res.json();
return json?.msg || fallback;
} catch {
return fallback;
}
}