feat: add Russian localization and independent STT languages
This commit is contained in:
+22
-17
@@ -3,6 +3,8 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Loader2Icon, MicIcon } from "lucide-react";
|
||||
import { useSettingsDialog } from "@/components/settings/settings-dialog";
|
||||
import { dictationUrl, type Language } from "@/src/core";
|
||||
import { LanguageSelect, useI18n } from "@/components/i18n";
|
||||
import { Hint } from "@/components/ui/hint";
|
||||
|
||||
/**
|
||||
@@ -86,15 +88,7 @@ import { Hint } from "@/components/ui/hint";
|
||||
* endpointing=300, no interims. Silence streams too (the worklet never
|
||||
* pauses), so the socket never idles — no KeepAlive needed.
|
||||
*/
|
||||
const DG_URL =
|
||||
"wss://api.deepgram.com/v1/listen" +
|
||||
"?model=nova-3" +
|
||||
"&encoding=linear16" +
|
||||
"&sample_rate=16000" +
|
||||
"&channels=1" +
|
||||
"&smart_format=true" +
|
||||
"&punctuate=true" +
|
||||
"&endpointing=300";
|
||||
|
||||
|
||||
/** Meter tuning — the owner's tested values, see the file header. */
|
||||
const METER_FLOOR_DB = -50;
|
||||
@@ -285,6 +279,10 @@ export type Dictation = {
|
||||
* question waits.
|
||||
*/
|
||||
export function useDictation({ append }: { append: (text: string) => void }): Dictation {
|
||||
const { sttLanguage, setDictating } = useI18n();
|
||||
const selectedLanguage = useRef(sttLanguage);
|
||||
selectedLanguage.current = sttLanguage;
|
||||
const recordingLanguage = useRef<Language>(sttLanguage);
|
||||
const [phase, setPhase] = useState<DictationPhase>("idle");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
/** Latest RMS from the worklet — the meter samples this (dictate pattern). */
|
||||
@@ -330,6 +328,7 @@ export function useDictation({ append }: { append: (text: string) => void }): Di
|
||||
const cleanup = useCallback(() => {
|
||||
genRef.current++;
|
||||
dictationActiveFlag = false;
|
||||
setDictating(false);
|
||||
flush(); // the deferred transcript appears when the run ends (owner round 2)
|
||||
if (safetyRef.current) {
|
||||
clearTimeout(safetyRef.current);
|
||||
@@ -366,7 +365,7 @@ export function useDictation({ append }: { append: (text: string) => void }): Di
|
||||
// Minted JWTs authenticate via the "bearer" subprotocol — ["token",
|
||||
// jwt] is rejected at the handshake (verified live; "token" is for
|
||||
// raw API keys only, as in the owner's pi dictate extension).
|
||||
const ws = new WebSocket(DG_URL, ["bearer", token]);
|
||||
const ws = new WebSocket(dictationUrl(recordingLanguage.current), ["bearer", token]);
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.addEventListener("open", () => {
|
||||
@@ -426,6 +425,8 @@ export function useDictation({ append }: { append: (text: string) => void }): Di
|
||||
);
|
||||
|
||||
const start = useCallback(async () => {
|
||||
recordingLanguage.current = selectedLanguage.current;
|
||||
setDictating(true);
|
||||
dictationActiveFlag = true;
|
||||
setError(null);
|
||||
setPhase("connecting");
|
||||
@@ -622,6 +623,7 @@ export function useDictation({ append }: { append: (text: string) => void }): Di
|
||||
* (full text in the tooltip), cleared on the next click.
|
||||
*/
|
||||
export function DictationBar({ dictation }: { dictation: Dictation }) {
|
||||
const { t } = useI18n();
|
||||
const { phase, error, levelRef, toggle, active, clearError } = dictation;
|
||||
const { open: openSettings } = useSettingsDialog();
|
||||
/** The keyless click (zero-keys wave, owner dictation trio): the errored
|
||||
@@ -673,12 +675,13 @@ export function DictationBar({ dictation }: { dictation: Dictation }) {
|
||||
|
||||
if (!active) {
|
||||
return (
|
||||
<div className="flex w-full min-w-0 items-center gap-1.5">
|
||||
<Hint
|
||||
// Errored: the button span truncates the line — the tooltip is its
|
||||
// full reveal (the keyless line KEEPS "Deepgram" — owner overrule:
|
||||
// it's the Keys row label, the truth the user needs). Otherwise the
|
||||
// quiet label + the live ⌘D binding.
|
||||
label={error ?? "Dictate"}
|
||||
label={error ? t(error) : t("Dictate")}
|
||||
action={error ? undefined : "toggle-dictation"}
|
||||
side="top"
|
||||
>
|
||||
@@ -692,16 +695,18 @@ export function DictationBar({ dictation }: { dictation: Dictation }) {
|
||||
toggle();
|
||||
}
|
||||
}}
|
||||
aria-label={error ? `Dictation failed: ${error}` : "Start dictation"}
|
||||
aria-label={error ? t("Dictation failed: {v0}", { v0: error }) : t("Start dictation")}
|
||||
data-testid="dictation-toggle"
|
||||
// Same blurring as the floating chrome (owner, fix wave round 3),
|
||||
// but lower opacity — the mic stays quieter than the model pills.
|
||||
className="flex h-7 w-full items-center justify-center gap-1.5 rounded-md border border-border/50 bg-card/50 text-xs text-muted-foreground/70 shadow-lg backdrop-blur transition-colors hover:border-border hover:text-muted-foreground"
|
||||
>
|
||||
<MicIcon className="size-3.5" />
|
||||
<span className="max-w-full truncate">{error ?? "Dictate"}</span>
|
||||
<span className="max-w-full truncate">{error ? t(error) : t("Dictate")}</span>
|
||||
</button>
|
||||
</Hint>
|
||||
<LanguageSelect field="sttLanguage" compact />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -710,14 +715,14 @@ export function DictationBar({ dictation }: { dictation: Dictation }) {
|
||||
<Hint
|
||||
// The owner flagged this copy good ("Stop dictation (Esc)") —
|
||||
// kept verbatim; only the machinery moved to the styled tooltip.
|
||||
label="Stop dictation (Esc)"
|
||||
label={t("Stop dictation (Esc)")}
|
||||
side="top"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
disabled={phase === "stopping"}
|
||||
aria-label="Stop dictation"
|
||||
aria-label={t("Stop dictation")}
|
||||
data-testid="dictation-toggle"
|
||||
className="flex h-7 shrink-0 items-center gap-1.5 rounded-md bg-destructive/10 px-2 text-xs font-medium text-destructive transition-colors hover:bg-destructive/20"
|
||||
>
|
||||
@@ -726,13 +731,13 @@ export function DictationBar({ dictation }: { dictation: Dictation }) {
|
||||
) : (
|
||||
<MicIcon className="size-3.5" />
|
||||
)}
|
||||
Stop
|
||||
</button>
|
||||
{t("Stop")}</button>
|
||||
</Hint>
|
||||
<span className="size-1.5 shrink-0 animate-pulse rounded-full bg-destructive" aria-hidden />
|
||||
{/* Fixed width while recording — nothing ever appends text beside it
|
||||
(no interims, owner round 1). Cells carry no minimum width; the
|
||||
COUNT adapts to the measured row space (owner round 2). */}
|
||||
<LanguageSelect field="sttLanguage" compact disabled />
|
||||
<div ref={meterRef} className="flex h-4 min-w-0 flex-1 items-center gap-[2px]" data-testid="dictation-meter">
|
||||
{levels.slice(-cellCount).map((v, i) => (
|
||||
<span
|
||||
|
||||
Reference in New Issue
Block a user