feat: add TTS voice picker to voice mode bottom sheet

This commit is contained in:
Gan, Jimmy
2026-02-26 16:22:16 +08:00
parent ee6c404122
commit 3d6fb06439
2 changed files with 30 additions and 3 deletions
+22 -3
View File
@@ -19,6 +19,8 @@ export class VoiceSession {
this.voiceMode = false;
this.transcript = "";
this.talkMode = "auto";
this.voices = [];
this.voiceIdx = -1;
this.term = null;
this.ws = null;
this._lastLine = 0;
@@ -32,6 +34,22 @@ export class VoiceSession {
get lang() { return LANGS[this.langIdx]; }
get langLabel() { return LANG_LABELS[this.langIdx]; }
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) {
this.term = term;
@@ -50,6 +68,8 @@ export class VoiceSession {
speechSynthesis.speak(new SpeechSynthesisUtterance(""));
this._ttsUnlocked = true;
}
this.loadVoices();
speechSynthesis.onvoiceschanged = () => this.loadVoices();
if (this.term) {
this._lastLine = this.term.buffer.active.baseY + this.term.buffer.active.cursorY;
}
@@ -193,9 +213,7 @@ export class VoiceSession {
speechSynthesis.cancel();
const u = new SpeechSynthesisUtterance(text);
u.lang = this.lang;
const voices = speechSynthesis.getVoices();
const v = voices.find(v => v.lang.startsWith(this.lang.split("-")[0]));
if (v) u.voice = v;
if (this.voices[this.voiceIdx]) u.voice = this.voices[this.voiceIdx];
u.onstart = () => { this.speaking = true; this._notify(); };
u.onend = () => { this.speaking = false; this._notify(); };
u.onerror = () => { this.speaking = false; this._notify(); };
@@ -215,6 +233,7 @@ export class VoiceSession {
cycleLang() {
this.langIdx = (this.langIdx + 1) % LANGS.length;
this.loadVoices();
if (this.listening) {
const wasContinuous = this._recog?.continuous;
this._stopSTT();
@@ -180,6 +180,14 @@
<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>
<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
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++; }}