feat: add TTS voice picker to voice mode bottom sheet
Deploy Dashboard / deploy (push) Successful in 1m8s

This commit is contained in:
Gan, Jimmy
2026-02-26 16:22:16 +08:00
parent f218d8df55
commit 46dfd8c7ac
2 changed files with 30 additions and 3 deletions
+22 -3
View File
@@ -19,6 +19,8 @@ export class VoiceSession {
this.voiceMode = false; this.voiceMode = false;
this.transcript = ""; this.transcript = "";
this.talkMode = "auto"; this.talkMode = "auto";
this.voices = [];
this.voiceIdx = -1;
this.term = null; this.term = null;
this.ws = null; this.ws = null;
this._lastLine = 0; this._lastLine = 0;
@@ -32,6 +34,22 @@ export class VoiceSession {
get lang() { return LANGS[this.langIdx]; } get lang() { return LANGS[this.langIdx]; }
get langLabel() { return LANG_LABELS[this.langIdx]; } get langLabel() { return LANG_LABELS[this.langIdx]; }
get modeLabel() { return MODE_LABELS[this.talkMode]; } get modeLabel() { return MODE_LABELS[this.talkMode]; }
get voiceName() { return this.voices[this.voiceIdx]?.name || "Default"; }
loadVoices() {
const all = speechSynthesis.getVoices();
const prefix = this.lang.split("-")[0];
this.voices = all.filter(v => v.lang.startsWith(prefix));
// Try to keep current selection
if (this.voiceIdx >= this.voices.length) this.voiceIdx = 0;
if (!this.voices.length) this.voiceIdx = -1;
this._notify();
}
setVoice(idx) {
this.voiceIdx = idx;
this._notify();
}
attach(term, ws) { attach(term, ws) {
this.term = term; this.term = term;
@@ -50,6 +68,8 @@ export class VoiceSession {
speechSynthesis.speak(new SpeechSynthesisUtterance("")); speechSynthesis.speak(new SpeechSynthesisUtterance(""));
this._ttsUnlocked = true; this._ttsUnlocked = true;
} }
this.loadVoices();
speechSynthesis.onvoiceschanged = () => this.loadVoices();
if (this.term) { if (this.term) {
this._lastLine = this.term.buffer.active.baseY + this.term.buffer.active.cursorY; this._lastLine = this.term.buffer.active.baseY + this.term.buffer.active.cursorY;
} }
@@ -193,9 +213,7 @@ export class VoiceSession {
speechSynthesis.cancel(); speechSynthesis.cancel();
const u = new SpeechSynthesisUtterance(text); const u = new SpeechSynthesisUtterance(text);
u.lang = this.lang; u.lang = this.lang;
const voices = speechSynthesis.getVoices(); if (this.voices[this.voiceIdx]) u.voice = this.voices[this.voiceIdx];
const v = voices.find(v => v.lang.startsWith(this.lang.split("-")[0]));
if (v) u.voice = v;
u.onstart = () => { this.speaking = true; this._notify(); }; u.onstart = () => { this.speaking = true; this._notify(); };
u.onend = () => { this.speaking = false; this._notify(); }; u.onend = () => { this.speaking = false; this._notify(); };
u.onerror = () => { this.speaking = false; this._notify(); }; u.onerror = () => { this.speaking = false; this._notify(); };
@@ -215,6 +233,7 @@ export class VoiceSession {
cycleLang() { cycleLang() {
this.langIdx = (this.langIdx + 1) % LANGS.length; this.langIdx = (this.langIdx + 1) % LANGS.length;
this.loadVoices();
if (this.listening) { if (this.listening) {
const wasContinuous = this._recog?.continuous; const wasContinuous = this._recog?.continuous;
this._stopSTT(); this._stopSTT();
@@ -180,6 +180,14 @@
<div class="flex items-center justify-between px-4 py-2 border-b border-surface-700/50"> <div class="flex items-center justify-between px-4 py-2 border-b border-surface-700/50">
<span class="text-sm font-medium text-surface-300">Voice Mode</span> <span class="text-sm font-medium text-surface-300">Voice Mode</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<select
class="bg-surface-700 text-surface-300 text-xs rounded px-1 py-0.5 outline-none max-w-[120px]"
onchange={(e) => { v.setVoice(+e.target.value); voiceTick++; }}
>
{#each v.voices as voice, i}
<option value={i} selected={i === v.voiceIdx}>{voice.name}</option>
{/each}
</select>
<button <button
class="px-1.5 py-0.5 rounded text-xs font-medium text-surface-400 hover:text-surface-200 hover:bg-surface-700" class="px-1.5 py-0.5 rounded text-xs font-medium text-surface-400 hover:text-surface-200 hover:bg-surface-700"
onclick={() => { v.cycleLang(); voiceTick++; }} onclick={() => { v.cycleLang(); voiceTick++; }}