.
This commit is contained in:
@@ -14,6 +14,7 @@ import { Button, CopyButton, Field, FlowerMark, Input, LoadingScreen, Picker, Sp
|
|||||||
import { cn } from "../lib/cn.ts";
|
import { cn } from "../lib/cn.ts";
|
||||||
import { msUntilNextSolarSwitch, resolveThemeDark } from "../lib/solarTheme.ts";
|
import { msUntilNextSolarSwitch, resolveThemeDark } from "../lib/solarTheme.ts";
|
||||||
import { useT, type StringKey } from "../lib/i18n.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";
|
type Tab = "accounts" | "access" | "reports" | "models" | "stats" | "features" | "mail";
|
||||||
|
|
||||||
@@ -349,6 +350,8 @@ function VoicePreviewsCard() {
|
|||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [msg, setMsg] = useState<{ ok: boolean; text: string } | null>(null);
|
const [msg, setMsg] = useState<{ ok: boolean; text: string } | null>(null);
|
||||||
const [serverVoices, setServerVoices] = useState<{ id: string; name: string }[]>([]);
|
const [serverVoices, setServerVoices] = useState<{ id: string; name: string }[]>([]);
|
||||||
|
const [ttsProvider, setTtsProvider] = useState<string | null>(null);
|
||||||
|
const [ttsAvailable, setTtsAvailable] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.adminGetSettings()
|
api.adminGetSettings()
|
||||||
@@ -357,7 +360,13 @@ function VoicePreviewsCard() {
|
|||||||
setLoaded(true);
|
setLoaded(true);
|
||||||
})
|
})
|
||||||
.catch(() => 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[]) {
|
async function save(next: VoicePreview[]) {
|
||||||
@@ -454,8 +463,8 @@ function VoicePreviewsCard() {
|
|||||||
</Field>
|
</Field>
|
||||||
</span>
|
</span>
|
||||||
<span className="w-40">
|
<span className="w-40">
|
||||||
<Field label={t("vfeat.voice")} hint={t("vfeat.voiceHint")}>
|
<Field label={t("vfeat.voice")} hint={ttsAvailable ? (ttsProvider ? `${ttsProvider} · ${t("vfeat.voiceHint")}` : t("vfeat.voiceHint")) : t("vfeat.noProvider")}>
|
||||||
{serverVoices.length > 0 ? (
|
{serverVoices.length > 0 || ttsProvider !== "elevenlabs" ? (
|
||||||
<Picker
|
<Picker
|
||||||
ariaLabel={t("vfeat.voice")}
|
ariaLabel={t("vfeat.voice")}
|
||||||
value={line.voice}
|
value={line.voice}
|
||||||
@@ -465,7 +474,12 @@ function VoicePreviewsCard() {
|
|||||||
if (next[i]!.text.trim() !== "") void save(next);
|
if (next[i]!.text.trim() !== "") void save(next);
|
||||||
}}
|
}}
|
||||||
align="left"
|
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
|
<Input
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { api, type UserProvider, type VoicePreview } from "../lib/api.ts";
|
|||||||
import { pushToast } from "../lib/toasts.ts";
|
import { pushToast } from "../lib/toasts.ts";
|
||||||
import { cn } from "../lib/cn.ts";
|
import { cn } from "../lib/cn.ts";
|
||||||
import { ACCENT_PRESETS, applyAccent, currentAccent, previewAccent, type AccentState } from "../lib/accent.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 { FEATURES, setFeature, useFeatures } from "../lib/features.ts";
|
||||||
import type { Conversation, FilePreview, User } from "../lib/types.ts";
|
import type { Conversation, FilePreview, User } from "../lib/types.ts";
|
||||||
import { useT, type StringKey } from "../lib/i18n.ts";
|
import { useT, type StringKey } from "../lib/i18n.ts";
|
||||||
@@ -978,8 +978,11 @@ function VoiceSection() {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
{(prefs.engine !== "browser") && (
|
{(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")}>
|
<Field label={t("voice.serverVoice")}>
|
||||||
<Picker
|
<Picker
|
||||||
ariaLabel={t("voice.serverVoice")}
|
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 }))]}
|
options={[{ value: "", label: t("voice.defaultVoice") }, ...ttsServer.voices.map((v) => ({ value: v.id, label: v.name }))]}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
) : ttsServer.provider === "elevenlabs" ? (
|
) : ttsServer?.provider === "elevenlabs" ? (
|
||||||
<Field label={t("voice.serverVoice")} hint={t("voice.serverVoiceHint")}>
|
<Field label={t("voice.serverVoice")} hint={t("voice.serverVoiceHint")}>
|
||||||
<Input
|
<Input
|
||||||
value={prefs.serverVoice ?? ""}
|
value={prefs.serverVoice ?? ""}
|
||||||
@@ -999,12 +1002,21 @@ function VoiceSection() {
|
|||||||
maxLength={64}
|
maxLength={64}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</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-stone-100 px-4 py-2.5 text-sm opacity-70 dark:bg-zinc-800">{t("voice.piperNote")}</p>
|
||||||
)
|
) : (
|
||||||
) : (
|
<Field label={t("voice.serverVoice")} hint={t("vfeat.noProvider")}>
|
||||||
<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>
|
<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")}>
|
<Field label={t("voice.voice")}>
|
||||||
<Picker
|
<Picker
|
||||||
|
|||||||
@@ -608,6 +608,8 @@ const en = {
|
|||||||
"vfeat.voice": "Server voice",
|
"vfeat.voice": "Server voice",
|
||||||
"vfeat.voiceHint": "ElevenLabs id / OpenAI name, empty = default",
|
"vfeat.voiceHint": "ElevenLabs id / OpenAI name, empty = default",
|
||||||
"vfeat.voicePh": "auto",
|
"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.ttsTitle": "Neural voice playback",
|
||||||
"center.ttsDesc": "Server text-to-speech for message playback (ElevenLabs / OpenAI / piper). Off = browser speech only.",
|
"center.ttsDesc": "Server text-to-speech for message playback (ElevenLabs / OpenAI / piper). Off = browser speech only.",
|
||||||
"voice.engine": "Playback engine",
|
"voice.engine": "Playback engine",
|
||||||
@@ -1316,6 +1318,8 @@ const es: Dict = {
|
|||||||
"vfeat.voice": "Server voice",
|
"vfeat.voice": "Server voice",
|
||||||
"vfeat.voiceHint": "ElevenLabs id / OpenAI name, empty = default",
|
"vfeat.voiceHint": "ElevenLabs id / OpenAI name, empty = default",
|
||||||
"vfeat.voicePh": "auto",
|
"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.ttsTitle": "Neural voice playback",
|
||||||
"center.ttsDesc": "Server text-to-speech for message playback (ElevenLabs / OpenAI / piper). Off = browser speech only.",
|
"center.ttsDesc": "Server text-to-speech for message playback (ElevenLabs / OpenAI / piper). Off = browser speech only.",
|
||||||
"voice.engine": "Playback engine",
|
"voice.engine": "Playback engine",
|
||||||
@@ -2019,6 +2023,8 @@ const fr: Dict = {
|
|||||||
"vfeat.voice": "Server voice",
|
"vfeat.voice": "Server voice",
|
||||||
"vfeat.voiceHint": "ElevenLabs id / OpenAI name, empty = default",
|
"vfeat.voiceHint": "ElevenLabs id / OpenAI name, empty = default",
|
||||||
"vfeat.voicePh": "auto",
|
"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.ttsTitle": "Neural voice playback",
|
||||||
"center.ttsDesc": "Server text-to-speech for message playback (ElevenLabs / OpenAI / piper). Off = browser speech only.",
|
"center.ttsDesc": "Server text-to-speech for message playback (ElevenLabs / OpenAI / piper). Off = browser speech only.",
|
||||||
"voice.engine": "Playback engine",
|
"voice.engine": "Playback engine",
|
||||||
@@ -2710,6 +2716,8 @@ const ru: Dict = {
|
|||||||
"vfeat.voice": "Server voice",
|
"vfeat.voice": "Server voice",
|
||||||
"vfeat.voiceHint": "ElevenLabs id / OpenAI name, empty = default",
|
"vfeat.voiceHint": "ElevenLabs id / OpenAI name, empty = default",
|
||||||
"vfeat.voicePh": "auto",
|
"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.ttsTitle": "Neural voice playback",
|
||||||
"center.ttsDesc": "Server text-to-speech for message playback (ElevenLabs / OpenAI / piper). Off = browser speech only.",
|
"center.ttsDesc": "Server text-to-speech for message playback (ElevenLabs / OpenAI / piper). Off = browser speech only.",
|
||||||
"voice.engine": "Playback engine",
|
"voice.engine": "Playback engine",
|
||||||
@@ -3406,6 +3414,8 @@ const it: Dict = {
|
|||||||
"vfeat.voice": "Server voice",
|
"vfeat.voice": "Server voice",
|
||||||
"vfeat.voiceHint": "ElevenLabs id / OpenAI name, empty = default",
|
"vfeat.voiceHint": "ElevenLabs id / OpenAI name, empty = default",
|
||||||
"vfeat.voicePh": "auto",
|
"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.ttsTitle": "Neural voice playback",
|
||||||
"center.ttsDesc": "Server text-to-speech for message playback (ElevenLabs / OpenAI / piper). Off = browser speech only.",
|
"center.ttsDesc": "Server text-to-speech for message playback (ElevenLabs / OpenAI / piper). Off = browser speech only.",
|
||||||
"voice.engine": "Playback engine",
|
"voice.engine": "Playback engine",
|
||||||
@@ -4093,6 +4103,8 @@ const pt: Dict = {
|
|||||||
"vfeat.voice": "Server voice",
|
"vfeat.voice": "Server voice",
|
||||||
"vfeat.voiceHint": "ElevenLabs id / OpenAI name, empty = default",
|
"vfeat.voiceHint": "ElevenLabs id / OpenAI name, empty = default",
|
||||||
"vfeat.voicePh": "auto",
|
"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.ttsTitle": "Neural voice playback",
|
||||||
"center.ttsDesc": "Server text-to-speech for message playback (ElevenLabs / OpenAI / piper). Off = browser speech only.",
|
"center.ttsDesc": "Server text-to-speech for message playback (ElevenLabs / OpenAI / piper). Off = browser speech only.",
|
||||||
"voice.engine": "Playback engine",
|
"voice.engine": "Playback engine",
|
||||||
|
|||||||
@@ -90,6 +90,9 @@ export interface SpeakOpts {
|
|||||||
|
|
||||||
export type VoiceTimbre = "masculine" | "feminine" | "any";
|
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,
|
// Name fragments hinting a masculine / feminine voice (Chrome, Edge,
|
||||||
// Safari and Android voices carry such markers; absence falls back to
|
// Safari and Android voices carry such markers; absence falls back to
|
||||||
// any same-language voice — never an error).
|
// any same-language voice — never an error).
|
||||||
|
|||||||
@@ -50,3 +50,6 @@ witht the flower itself following thr accentic color
|
|||||||
-- in specify
|
-- in specify
|
||||||
specify the model used in each footer of assistant message
|
specify the model used in each footer of assistant message
|
||||||
like <model> <button>
|
like <model> <button>
|
||||||
|
|
||||||
|
|
||||||
|
---- pouvoir dans admin centers, sur users, envoyez un lien mdp reset à un user
|
||||||
|
|||||||
Reference in New Issue
Block a user