feat: add TTS voice picker to voice mode bottom sheet
Deploy Dashboard / deploy (push) Successful in 1m8s
Deploy Dashboard / deploy (push) Successful in 1m8s
This commit is contained in:
@@ -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++; }}
|
||||
|
||||
Reference in New Issue
Block a user