This commit is contained in:
k
2026-09-24 11:42:53 +02:00
parent 30918f33e7
commit ac04c674a6
5 changed files with 57 additions and 13 deletions
+18 -4
View File
@@ -14,6 +14,7 @@ import { Button, CopyButton, Field, FlowerMark, Input, LoadingScreen, Picker, Sp
import { cn } from "../lib/cn.ts";
import { msUntilNextSolarSwitch, resolveThemeDark } from "../lib/solarTheme.ts";
import { useT, type StringKey } from "../lib/i18n.ts";
import { OPENAI_STATIC_VOICES } from "../lib/voice.ts";
type Tab = "accounts" | "access" | "reports" | "models" | "stats" | "features" | "mail";
@@ -349,6 +350,8 @@ function VoicePreviewsCard() {
const [busy, setBusy] = useState(false);
const [msg, setMsg] = useState<{ ok: boolean; text: string } | null>(null);
const [serverVoices, setServerVoices] = useState<{ id: string; name: string }[]>([]);
const [ttsProvider, setTtsProvider] = useState<string | null>(null);
const [ttsAvailable, setTtsAvailable] = useState(false);
useEffect(() => {
api.adminGetSettings()
@@ -357,7 +360,13 @@ function VoicePreviewsCard() {
setLoaded(true);
})
.catch(() => setLoaded(true));
api.ttsInfo().then((r) => setServerVoices(r.voices)).catch(() => {});
api.ttsInfo()
.then((r) => {
setServerVoices(r.voices);
setTtsProvider(r.provider);
setTtsAvailable(r.available);
})
.catch(() => {});
}, []);
async function save(next: VoicePreview[]) {
@@ -454,8 +463,8 @@ function VoicePreviewsCard() {
</Field>
</span>
<span className="w-40">
<Field label={t("vfeat.voice")} hint={t("vfeat.voiceHint")}>
{serverVoices.length > 0 ? (
<Field label={t("vfeat.voice")} hint={ttsAvailable ? (ttsProvider ? `${ttsProvider} · ${t("vfeat.voiceHint")}` : t("vfeat.voiceHint")) : t("vfeat.noProvider")}>
{serverVoices.length > 0 || ttsProvider !== "elevenlabs" ? (
<Picker
ariaLabel={t("vfeat.voice")}
value={line.voice}
@@ -465,7 +474,12 @@ function VoicePreviewsCard() {
if (next[i]!.text.trim() !== "") void save(next);
}}
align="left"
options={[{ value: "", label: t("vfeat.voicePh") }, ...serverVoices.map((v) => ({ value: v.id, label: v.name }))]}
options={[
{ value: "", label: t("vfeat.voicePh") },
...(serverVoices.length > 0
? serverVoices.map((v) => ({ value: v.id, label: v.name }))
: OPENAI_STATIC_VOICES.map((v) => ({ value: v, label: v }))),
]}
/>
) : (
<Input
+21 -9
View File
@@ -4,7 +4,7 @@ import { api, type UserProvider, type VoicePreview } from "../lib/api.ts";
import { pushToast } from "../lib/toasts.ts";
import { cn } from "../lib/cn.ts";
import { ACCENT_PRESETS, applyAccent, currentAccent, previewAccent, type AccentState } from "../lib/accent.ts";
import { detectLang, getVoicePrefs, listTtsVoices, pickVoice, playText, setVoicePrefs, stopPlayback, ttsSupported, type VoicePrefs } from "../lib/voice.ts";
import { detectLang, getVoicePrefs, listTtsVoices, OPENAI_STATIC_VOICES, pickVoice, playText, setVoicePrefs, stopPlayback, ttsSupported, type VoicePrefs } from "../lib/voice.ts";
import { FEATURES, setFeature, useFeatures } from "../lib/features.ts";
import type { Conversation, FilePreview, User } from "../lib/types.ts";
import { useT, type StringKey } from "../lib/i18n.ts";
@@ -978,8 +978,11 @@ function VoiceSection() {
/>
</Field>
{(prefs.engine !== "browser") && (
ttsServer?.available ? (
ttsServer.voices.length > 0 ? (
<>
{!ttsServer?.available && (
<p className="rounded-2xl bg-amber-500/10 px-4 py-2.5 text-sm text-amber-700 dark:text-amber-400">{t("voice.noServer")}</p>
)}
{ttsServer && ttsServer.voices.length > 0 ? (
<Field label={t("voice.serverVoice")}>
<Picker
ariaLabel={t("voice.serverVoice")}
@@ -990,7 +993,7 @@ function VoiceSection() {
options={[{ value: "", label: t("voice.defaultVoice") }, ...ttsServer.voices.map((v) => ({ value: v.id, label: v.name }))]}
/>
</Field>
) : ttsServer.provider === "elevenlabs" ? (
) : ttsServer?.provider === "elevenlabs" ? (
<Field label={t("voice.serverVoice")} hint={t("voice.serverVoiceHint")}>
<Input
value={prefs.serverVoice ?? ""}
@@ -999,12 +1002,21 @@ function VoiceSection() {
maxLength={64}
/>
</Field>
) : (
) : ttsServer?.provider === "piper" ? (
<p className="rounded-2xl bg-stone-100 px-4 py-2.5 text-sm opacity-70 dark:bg-zinc-800">{t("voice.piperNote")}</p>
)
) : (
<p className="rounded-2xl bg-amber-500/10 px-4 py-2.5 text-sm text-amber-700 dark:text-amber-400">{t("voice.noServer")}</p>
)
) : (
<Field label={t("voice.serverVoice")} hint={t("vfeat.noProvider")}>
<Picker
ariaLabel={t("voice.serverVoice")}
icon={Volume2}
value={prefs.serverVoice ?? ""}
onChange={(v) => update({ serverVoice: v || null })}
align="left"
options={[{ value: "", label: t("voice.defaultVoice") }, ...OPENAI_STATIC_VOICES.map((v) => ({ value: v, label: v }))]}
/>
</Field>
)}
</>
)}
<Field label={t("voice.voice")}>
<Picker
+12
View File
@@ -608,6 +608,8 @@ const en = {
"vfeat.voice": "Server voice",
"vfeat.voiceHint": "ElevenLabs id / OpenAI name, empty = default",
"vfeat.voicePh": "auto",
"vfeat.noProvider": "No neural provider yet — add ELEVENLABS_API_KEY or OPENAI_API_KEY to activate.",
"center.ttsTitle": "Neural voice playback",
"center.ttsDesc": "Server text-to-speech for message playback (ElevenLabs / OpenAI / piper). Off = browser speech only.",
"voice.engine": "Playback engine",
@@ -1316,6 +1318,8 @@ const es: Dict = {
"vfeat.voice": "Server voice",
"vfeat.voiceHint": "ElevenLabs id / OpenAI name, empty = default",
"vfeat.voicePh": "auto",
"vfeat.noProvider": "No neural provider yet — add ELEVENLABS_API_KEY or OPENAI_API_KEY to activate.",
"center.ttsTitle": "Neural voice playback",
"center.ttsDesc": "Server text-to-speech for message playback (ElevenLabs / OpenAI / piper). Off = browser speech only.",
"voice.engine": "Playback engine",
@@ -2019,6 +2023,8 @@ const fr: Dict = {
"vfeat.voice": "Server voice",
"vfeat.voiceHint": "ElevenLabs id / OpenAI name, empty = default",
"vfeat.voicePh": "auto",
"vfeat.noProvider": "No neural provider yet — add ELEVENLABS_API_KEY or OPENAI_API_KEY to activate.",
"center.ttsTitle": "Neural voice playback",
"center.ttsDesc": "Server text-to-speech for message playback (ElevenLabs / OpenAI / piper). Off = browser speech only.",
"voice.engine": "Playback engine",
@@ -2710,6 +2716,8 @@ const ru: Dict = {
"vfeat.voice": "Server voice",
"vfeat.voiceHint": "ElevenLabs id / OpenAI name, empty = default",
"vfeat.voicePh": "auto",
"vfeat.noProvider": "No neural provider yet — add ELEVENLABS_API_KEY or OPENAI_API_KEY to activate.",
"center.ttsTitle": "Neural voice playback",
"center.ttsDesc": "Server text-to-speech for message playback (ElevenLabs / OpenAI / piper). Off = browser speech only.",
"voice.engine": "Playback engine",
@@ -3406,6 +3414,8 @@ const it: Dict = {
"vfeat.voice": "Server voice",
"vfeat.voiceHint": "ElevenLabs id / OpenAI name, empty = default",
"vfeat.voicePh": "auto",
"vfeat.noProvider": "No neural provider yet — add ELEVENLABS_API_KEY or OPENAI_API_KEY to activate.",
"center.ttsTitle": "Neural voice playback",
"center.ttsDesc": "Server text-to-speech for message playback (ElevenLabs / OpenAI / piper). Off = browser speech only.",
"voice.engine": "Playback engine",
@@ -4093,6 +4103,8 @@ const pt: Dict = {
"vfeat.voice": "Server voice",
"vfeat.voiceHint": "ElevenLabs id / OpenAI name, empty = default",
"vfeat.voicePh": "auto",
"vfeat.noProvider": "No neural provider yet — add ELEVENLABS_API_KEY or OPENAI_API_KEY to activate.",
"center.ttsTitle": "Neural voice playback",
"center.ttsDesc": "Server text-to-speech for message playback (ElevenLabs / OpenAI / piper). Off = browser speech only.",
"voice.engine": "Playback engine",
+3
View File
@@ -90,6 +90,9 @@ export interface SpeakOpts {
export type VoiceTimbre = "masculine" | "feminine" | "any";
/** Stock OpenAI voices (static catalog — selectable even before any key). */
export const OPENAI_STATIC_VOICES = ["alloy", "echo", "fable", "onyx", "nova", "shimmer"];
// Name fragments hinting a masculine / feminine voice (Chrome, Edge,
// Safari and Android voices carry such markers; absence falls back to
// any same-language voice — never an error).
+3
View File
@@ -50,3 +50,6 @@ witht the flower itself following thr accentic color
-- in specify
specify the model used in each footer of assistant message
like <model> <button>
---- pouvoir dans admin centers, sur users, envoyez un lien mdp reset à un user