opencode-voice-dictation 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -4
- package/dist/gui-experimental.cjs +1 -1
- package/package.json +12 -6
- package/src/gui/main.ts +2 -2
- package/src/gui/settings.tsx +1 -1
- package/src/gui/speak.ts +2 -75
- package/src/{gui/engines/sapi-speech.ts → sapi-speech.ts} +1 -1
- package/src/speech-text.ts +75 -0
- package/src/tui/player.ts +128 -0
- package/src/tui.ts +253 -0
- /package/src/{gui/engines/speak-script.ts → speak-script.ts} +0 -0
- /package/src/{gui/engines/win-h.ts → win-h.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
# Dictado por voz para OpenCode Desktop
|
|
1
|
+
# Dictado por voz para OpenCode Desktop y TUI
|
|
2
2
|
|
|
3
3
|
Plugin de [OpenCode](https://opencode.ai) V2 que añade voz a la app de
|
|
4
|
-
escritorio:
|
|
4
|
+
escritorio y a la TUI:
|
|
5
5
|
|
|
6
6
|
- **Dictado**: pulsas la píldora y se abre el dictado de Windows 11 (Win+H),
|
|
7
7
|
que escribe en el prompt con su motor neuronal. También puedes usar el motor
|
|
8
8
|
offline `System.Speech` desde Ajustes.
|
|
9
9
|
- **Lectura**: pulsa «Leer» y reproduce en voz alta la última respuesta.
|
|
10
|
+
- **TUI**: usa `/dictado` y `/leer` (o `<leader>d` y `<leader>v`) en la terminal.
|
|
10
11
|
|
|
11
12
|
- **Dictado preciso**: por defecto delega en Windows Voice Typing (Win+H).
|
|
12
13
|
- **Modo offline**: `System.Speech` sigue disponible como alternativa sin
|
|
@@ -28,8 +29,8 @@ escritorio:
|
|
|
28
29
|
opencode plugin add opencode-voice-dictation
|
|
29
30
|
```
|
|
30
31
|
|
|
31
|
-
Reinicia OpenCode Desktop. El plugin queda registrado en el
|
|
32
|
-
global del entorno donde ejecutes el comando.
|
|
32
|
+
Reinicia OpenCode Desktop o la TUI. El plugin queda registrado en el
|
|
33
|
+
`opencode.json` global del entorno donde ejecutes el comando.
|
|
33
34
|
|
|
34
35
|
Actualizar:
|
|
35
36
|
|
|
@@ -44,6 +45,20 @@ está commiteado):
|
|
|
44
45
|
opencode plugin add github:evious/opencode-voice-dictation
|
|
45
46
|
```
|
|
46
47
|
|
|
48
|
+
## TUI (terminal)
|
|
49
|
+
|
|
50
|
+
El paquete también exporta una parte TUI. En Windows puedes dictar y escuchar
|
|
51
|
+
la última respuesta sin salir de la terminal:
|
|
52
|
+
|
|
53
|
+
| Comando | Atajo | Acción |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| `/dictado` | `<leader>d` (`Ctrl+X`, luego `d`) | Abre el dictado de Windows (Win+H) en el prompt. |
|
|
56
|
+
| `/leer` | `<leader>v` (`Ctrl+X`, luego `v`) | Lee la última respuesta. Si ya está leyendo, la detiene. |
|
|
57
|
+
|
|
58
|
+
La lectura usa las voces neuronales de Edge y, si falla la red o la
|
|
59
|
+
reproducción, cae en la voz del sistema. El dictado de la TUI requiere Windows
|
|
60
|
+
por ahora; Linux se añadirá más adelante.
|
|
61
|
+
|
|
47
62
|
## Compatibilidad
|
|
48
63
|
|
|
49
64
|
Probado con OpenCode Desktop **2.2.6** en Windows 11 y Ubuntu 24.04. La API de
|
|
@@ -189,4 +189,4 @@ public static class WinH {
|
|
|
189
189
|
[WinH]::Send()
|
|
190
190
|
`,_e="powershell.exe";function Ve(){const t=Q.join(ae.tmpdir(),"opencode-voice-dictation");K.mkdirSync(t,{recursive:!0});const e=oe.createHash("sha1").update(he).digest("hex").slice(0,12),i=Q.join(t,`win-h-${e}.ps1`);return K.writeFileSync(i,he,"utf8"),i}function ze(t){return ie.spawn(_e,t,{windowsHide:!0,stdio:"ignore"})}function De(){return new Promise((t,e)=>{const i=ze(["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-File",Ve()]);i.on("error",c=>e(c)),i.on("close",c=>{c===0?t():e(new Error(`Win+H helper exited with code ${c??"unknown"}`))})})}const Fe=t=>{let e="idle",i="",c="",n,s=!1;const u=()=>{n=void 0,s=!1},a=t.provide(le,{state:()=>({status:e,error:i,locale:c}),start(S,R){n&&(s=!0,n.stop(),u()),e="starting",i="",c=S.locale,a.changed();const v=R.window;try{n=Pe({locale:S.locale,minConfidence:S.minConfidence},{onReady(C){e="listening",c=C,a.changed()},onFinal(C,I){a.emit("final",{text:C,confidence:I},v)},onError(C,I,z){e="error",i=C,a.changed(),a.emit("error",{code:C,detail:I,available:z},v)},onExit(C,I){if(s){u(),e="idle",i="",c="",a.changed();return}u(),e!=="error"&&(e="error",i=C===null?"ENGINE_EXIT":`ENGINE_EXIT_${C}`,a.changed(),a.emit("error",{code:"ENGINE_EXIT",detail:I||(C===null?"":String(C)),available:[]},v))}})}catch(C){throw e="error",i="ENGINE_START_FAILED",a.changed(),C}},stop(){n&&(s=!0,n.stop(),u()),e="idle",i="",c="",a.changed()},diagnose(){return Ie()}});let g;const h=t.provide(fe,{speak(S){g?.stop(),g=Ne(S,R=>{g=void 0,h.emit("done",{ok:R})})},stop(){g&&(g.stop(),g=void 0)},voices(){return Le()}});t.provide(ce,{toggle(){return De()}}),t.onCleanup(()=>{n&&(s=!0,n.stop(),u()),g?.stop(),g=void 0})},j={id:"voice-dictation.voice",methods:{edgeVoices:{input:{type:"object",additionalProperties:!1},output:{type:"array",items:{type:"object",properties:{id:{type:"string"},label:{type:"string"},locale:{type:"string"},gender:{type:"string"}},required:["id","label","locale","gender"],additionalProperties:!1}}},edgeSpeak:{input:{type:"object",properties:{text:{type:"string"},voice:{type:"string"},rate:{type:"number"}},required:["text","voice","rate"],additionalProperties:!1},output:{type:"string"}},whisperTranscribe:{input:{type:"object",properties:{audio:{type:"string"},mime:{type:"string"},language:{type:"string"},python:{type:"string"},model:{type:"string"}},required:["audio","mime","language","python","model"],additionalProperties:!1},output:{type:"object",properties:{text:{type:"string"},language:{type:"string"}},required:["text","language"],additionalProperties:!1}},whisperStatus:{input:{type:"object",properties:{python:{type:"string"},model:{type:"string"}},required:["python","model"],additionalProperties:!1},output:{type:"object",properties:{ready:{type:"boolean"},model:{type:"string"},device:{type:"string"},error:{type:"string"}},required:["ready","model","device","error"],additionalProperties:!1}},capabilities:{input:{type:"object",additionalProperties:!1},output:{type:"object",properties:{platform:{type:"string"},engines:{type:"array",items:{type:"string"}}},required:["platform","engines"],additionalProperties:!1}},whisperCppInstall:{input:{type:"object",properties:{model:{type:"string"}},required:["model"],additionalProperties:!1},output:{type:"object",properties:{ok:{type:"boolean"},model:{type:"string"},error:{type:"string"}},required:["ok","model","error"],additionalProperties:!1}},whisperCppStatus:{input:{type:"object",properties:{model:{type:"string"}},required:["model"],additionalProperties:!1},output:{type:"object",properties:{installed:{type:"boolean"},ready:{type:"boolean"},model:{type:"string"},error:{type:"string"},ffmpeg:{type:"boolean"},ffmpegVersion:{type:"string"},ffmpegPath:{type:"string"}},required:["installed","ready","model","error","ffmpeg","ffmpegVersion","ffmpegPath"],additionalProperties:!1}},whisperCppInstallFfmpeg:{input:{type:"object",additionalProperties:!1},output:{type:"object",properties:{ok:{type:"boolean"},error:{type:"string"},command:{type:"string"}},required:["ok","error","command"],additionalProperties:!1}},whisperCppTranscribe:{input:{type:"object",properties:{audio:{type:"string"},mime:{type:"string"},language:{type:"string"},model:{type:"string"}},required:["audio","mime","language","model"],additionalProperties:!1},output:{type:"object",properties:{text:{type:"string"},language:{type:"string"}},required:["text","language"],additionalProperties:!1}}},events:{}},Oe=250;function je(t,e){let i=t.replace(/\r\n/g,`
|
|
191
191
|
`);return e?i=i.replace(/```[\s\S]*?```/g," "):i=i.replace(/```[^\n]*\n([\s\S]*?)```/g,"$1"),i=i.replace(/`([^`]+)`/g,"$1"),i=i.replace(/!\[[^\]]*\]\([^)]*\)/g," "),i=i.replace(/\[([^\]]+)\]\([^)]*\)/g,"$1"),i=i.replace(/^\s*#{1,6}\s+/gm,""),i=i.replace(/^\s*>\s?/gm,""),i=i.replace(/^\s*[-*+]\s+/gm,""),i=i.replace(/^\s*\d+\.\s+/gm,""),i=i.replace(/(\*\*|__)(.*?)\1/g,"$2"),i=i.replace(/(\*|_)(.*?)\1/g,"$2"),i=i.replace(/^\s*\|.*\|\s*$/gm," "),i=i.replace(/\n{2,}/g,". "),i=i.replace(/\n/g," "),i=i.replace(/\s+/g," "),i=i.replace(/\s+([.,;:!?])/g,"$1"),i=i.replace(/([.,;:!?]){2,}/g,"$1"),i.trim()}function me(t,e=Oe){const i=t.trim();if(!i)return[];const c=i.split(new RegExp("(?<=[.!?…])\\s+")),n=[];let s="";const u=a=>{let g="";for(const h of a.split(/\s+/))h&&(g&&g.length+1+h.length>e?(n.push(g),g=h):g=g?`${g} ${h}`:h);return g};for(const a of c){if(a.length>e){s&&(n.push(s),s=""),s=u(a);continue}s&&s.length+1+a.length>e?(n.push(s),s=a):s=s?`${s} ${a}`:a}return s&&n.push(s),n}function Be(t,e){const i=t.stores.speech,c=t.stores.tts.value,[n,s]=y.createSignal(c.engine==="edge"?"edge":"system"),[u,a]=y.createSignal(c.voice),g=w=>{(w.engine==="edge"||w.engine==="system")&&s(w.engine),w.voice!==void 0&&a(w.voice)},[h,S]=y.createSignal(!1),[R,v]=y.createSignal(!1),[C,I]=y.createSignal([]),[z,B]=y.createSignal(""),[N,U]=y.createSignal("none");let A=0,q=[],Z="",L,F;const T=new Map,W=()=>typeof globalThis.speechSynthesis<"u"?globalThis.speechSynthesis:void 0,x=()=>{const w=W()?.getVoices()??[];if(w.length===0)return;const f=i.value.voiceURI;if(f){const P=w.find(X=>X.voiceURI===f);if(P)return P}const k=t.locale.locale().split("-")[0]?.toLowerCase()??"es";return w.find(P=>P.lang.toLowerCase().startsWith(k))??w[0]},_=async()=>{if(n()==="edge"){try{const f=await t.client.rpc(j).edgeVoices({});f.length>0?(I(f.map(k=>({id:k.id,label:k.label}))),B("")):B(t.t("speech.edge.error"))}catch(f){B(f instanceof Error?f.message:String(f))}U("edge");return}B("");const w=W()?.getVoices()??[];if(w.length>0){I(w.map(f=>({id:f.voiceURI,label:`${f.name} — ${f.lang}`}))),U("web");return}try{const f=await e.voices();if(f.length>0){I(f.map(k=>({id:k,label:k}))),U("sapi");return}}catch{}U("none")},H=()=>{L&&(L.onended=null,L.onerror=null,L.pause(),L=void 0),F&&(URL.revokeObjectURL(F),F=void 0)},l=()=>{A++,q=[],W()?.cancel(),H(),v(!1),S(!1),N()==="sapi"&&e.stop().catch(()=>{})},m=()=>N()==="web"||N()==="edge",b=()=>{if(!(!h()||R()||!m())){if(N()==="edge"){L?.pause(),v(!0);return}W()?.pause(),v(!0)}},E=()=>{if(!(!h()||!R()||!m())){if(N()==="edge"){L?.play().catch(()=>{}),v(!1);return}W()?.resume(),v(!1)}},ee=()=>{R()?E():b()},M=()=>Z?G(Z):Promise.resolve(!1),O=w=>{const f=u()||"es-ES-XimenaNeural",k=i.value.rate,P=me(w,4e3),X=`${f}|${k}|${w}`,Y=T.get(X)??[];T.clear(),T.set(X,Y);const V=A;S(!0),v(!1);const r=d=>{if(V===A){if(d>=P.length){v(!1),S(!1);return}(async()=>{let D=Y[d];try{D||(D=await t.client.rpc(j).edgeSpeak({text:P[d]??"",voice:f,rate:k}),Y[d]=D)}catch{V===A&&(v(!1),S(!1),t.toast.show({variant:"error",title:t.t("read.title"),description:t.t("error.edge")}));return}if(V!==A)return;const re=Uint8Array.from(atob(D),Ee=>Ee.charCodeAt(0)),de=URL.createObjectURL(new Blob([re],{type:"audio/mpeg"}));H(),F=de;const ne=new Audio(de);L=ne,ne.onended=()=>{V===A&&(H(),r(d+1))},ne.onerror=()=>{V===A&&(H(),v(!1),S(!1))},await ne.play().catch(()=>{V===A&&(H(),v(!1),S(!1),t.toast.show({variant:"error",title:t.t("read.title"),description:t.t("error.edge")}))})})()}};return r(0),!0},G=async w=>{Z=w;const f=je(w,i.value.skipCode);if(!f)return!1;if(l(),await _(),n()==="edge")return O(f);if(N()==="web"){const k=W();if(!k)return!1;const P=x(),X=i.value.rate,Y=A;q=me(f),S(!0),v(!1);const V=()=>{if(Y!==A)return;const r=q.shift();if(!r){v(!1),S(!1);return}const d=new SpeechSynthesisUtterance(r);P&&(d.voice=P),P?.lang&&(d.lang=P.lang),d.rate=X,d.onend=V,d.onerror=V,k.speak(d)};return V(),!0}if(N()==="sapi"){S(!0),v(!1);try{await e.speak({text:f,rate:i.value.rate,voice:i.value.voiceURI})}catch(k){throw S(!1),k}return!0}throw S(!1),new Error(t.t("read.noVoices"))},J=async()=>{const w=t.sessions.current();if(!w)return"";const k=(await w.server.client.message.list({sessionID:w.id,type:"assistant",order:"desc",limit:1})).data[0];return!k||k.type!=="assistant"?"":k.content.filter(P=>P.type==="text").map(P=>P.text).join(`
|
|
192
|
-
`)};return e.on("done",()=>{v(!1),S(!1)}),W()?.addEventListener?.("voiceschanged",()=>{_()}),_(),{speaking:h,paused:R,backend:N,engine:n,voice:u,error:z,canPause:m,setConfig:g,voices:C,refreshVoices:_,lastAssistantText:J,speak:G,cancel:l,pause:b,resume:E,togglePause:ee,restart:M,preview:async()=>{const w=await G(t.t("speech.preview.text"));if(w){const f=n()==="edge"?`Edge · ${u()}`:`${N()} · ${i.value.voiceURI||"auto"}`;t.toast.show({variant:"default",title:t.t("read.title"),description:f})}return w}}}function te(t,e){if(t instanceof Error&&t.message)return t.message;if(typeof t=="string"&&t)return t;if(t&&typeof t=="object"){const i=t;for(const n of["message","error","detail","defect"]){const s=i[n];if(typeof s=="string"&&s)return s}const c=i.data;if(c&&typeof c=="object"){const n=c.message;if(typeof n=="string"&&n)return n}try{const n=JSON.stringify(t);if(n&&n!=="{}")return n}catch{return e}}return e}const Ue=["es-ES","es-MX","es-AR","es-CO","pt-BR","pt-PT","en-US","en-GB","fr-FR","de-DE","it-IT"];function qe(t){const e=$.usePlugin(),i=e.ui,c=e.stores.prefs,n=e.stores.speech,s=e.stores.tts,u=e.stores.engine,a=e.stores.whisper,g=e.stores.whispercpp,h=e.main(le),S=e.main(ce),[R,v]=y.createSignal([]),[C,I]=y.createSignal(!1),[z,B]=y.createSignal(!1),[N,U]=y.createSignal(!1),[A,q]=y.createSignal(!1),[Z,L]=y.createSignal(!1),[F,T]=y.createSignal(void 0),[W,x]=y.createSignal(""),_=async()=>{const r=await e.client.rpc(j).whisperCppStatus({model:g.value.model});return T({available:r.ffmpeg,version:r.ffmpegVersion}),r},H=()=>{const r=F();return r?r.available?e.t("whispercpp.ffmpeg.ready",{version:r.version}):e.t("whispercpp.ffmpeg.missing"):e.t("whispercpp.ffmpeg.description")};y.onMount(()=>{t.speaker.setConfig({engine:s.value.engine,voice:s.value.voice});const r=()=>{h.diagnose().then(d=>{d.locales.length>0&&v(d.locales)}).catch(()=>{})};e.client.rpc(j).capabilities({}).then(d=>{const D=d;x(D.platform),!D.engines.includes(u.value.mode)&&D.engines.length>0&&G(D.engines[0]??"windows"),D.platform!=="linux"?r():_().catch(()=>{})}).catch(()=>r()),t.speaker.refreshVoices()});const l=()=>W()==="linux"?[{value:"whispercpp",label:e.t("engine.whispercpp")}]:[{value:"windows",label:e.t("engine.windows")},{value:"system",label:e.t("engine.system")},{value:"whisper",label:e.t("engine.whisper")}],m=()=>{const r=R(),d=c.value.locale;return(r.includes(d)?r:[d,...r]).map(re=>({value:re,label:re}))},b=()=>{const r=t.speaker.voices().map(d=>({value:d.id,label:d.label}));return s.value.engine==="edge"?r:[{value:"",label:e.t("speech.voice.auto")},...r]},E=()=>s.value.engine==="edge"?s.value.voice:n.value.voiceURI,ee=r=>{if(s.value.engine==="edge"){s.update(d=>{d.voice=r}),t.speaker.setConfig({voice:r});return}O({voiceURI:r})},M=r=>{c.update(d=>{r.locale!==void 0&&(d.locale=r.locale),r.minConfidence!==void 0&&(d.minConfidence=r.minConfidence),r.trailingSpace!==void 0&&(d.trailingSpace=r.trailingSpace),r.autoFocus!==void 0&&(d.autoFocus=r.autoFocus)})},O=r=>{n.update(d=>{r.voiceURI!==void 0&&(d.voiceURI=r.voiceURI),r.rate!==void 0&&(d.rate=r.rate),r.skipCode!==void 0&&(d.skipCode=r.skipCode)})},G=r=>{u.update(d=>{d.mode=r})},J=r=>{a.update(d=>{r.python!==void 0&&(d.python=r.python),r.model!==void 0&&(d.model=r.model),r.language!==void 0&&(d.language=r.language)})},se=r=>{g.update(d=>{r.model!==void 0&&(d.model=r.model),r.language!==void 0&&(d.language=r.language)})},pe=async()=>{U(!0);try{const r=await e.client.rpc(j).whisperCppInstall({model:g.value.model});r.ok?(e.toast.show({variant:"success",title:e.t("pill.title"),description:e.t("whispercpp.install.ok",{model:r.model})}),await _().catch(()=>{})):e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.install.error",{error:r.error||"?"}),persistent:!0})}catch(r){e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.install.error",{error:te(r,"?")}),persistent:!0})}finally{U(!1)}},w=async()=>{q(!0);try{const r=await _();r.error?e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.test.error",{error:r.error}),persistent:!0}):r.installed?r.ffmpeg?e.toast.show({variant:"success",title:e.t("pill.title"),description:e.t("whispercpp.test.ready",{model:r.model})}):e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.ffmpeg.missing"),persistent:!0}):e.toast.show({variant:"default",title:e.t("pill.title"),description:e.t("whispercpp.test.missing")})}catch(r){e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.test.error",{error:te(r,"?")})})}finally{q(!1)}},f=async()=>{L(!0);try{const r=await e.client.rpc(j).whisperCppInstallFfmpeg({});if(r.ok)e.toast.show({variant:"success",title:e.t("pill.title"),description:e.t("whispercpp.ffmpeg.install.ok")}),await _();else{const d=r.command?` ${e.t("whispercpp.ffmpeg.install.manual",{command:r.command})}`:"";e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.ffmpeg.install.error",{error:r.error||"?"})+d,persistent:!0})}}catch(r){e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.ffmpeg.install.error",{error:te(r,"?")}),persistent:!0})}finally{L(!1)}},k=async()=>{B(!0);try{const r=await e.client.rpc(j).whisperStatus({python:a.value.python,model:a.value.model});r.ready?e.toast.show({variant:"success",title:e.t("pill.title"),description:e.t("whisper.test.ready",{model:r.model,device:r.device})}):e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whisper.test.error",{error:r.error||"?"})})}catch(r){e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whisper.test.error",{error:r instanceof Error?r.message:String(r)})})}finally{B(!1)}},P=r=>{s.update(d=>{d.engine=r}),t.speaker.setConfig({engine:r}),t.speaker.refreshVoices()},X=async()=>{try{await S.toggle(),e.toast.show({variant:"default",title:e.t("settings.winh"),description:e.t("settings.winh.sent")})}catch(r){e.toast.show({variant:"error",title:e.t("pill.title"),description:r instanceof Error?r.message:e.t("error.winh")})}},Y=async()=>{try{await t.speaker.preview()}catch(r){e.toast.show({variant:"error",title:e.t("read.title"),description:r instanceof Error?r.message:e.t("read.error")})}},V=async()=>{I(!0);try{const r=await h.diagnose();if(r.locales.length===0){e.toast.show({variant:"error",title:e.t("error.title"),description:e.t("settings.test.failed",{reason:e.t("common.reason.no-recognizers")}),persistent:!0});return}if(!r.microphone){e.toast.show({variant:"error",title:e.t("error.title"),description:e.t("settings.test.failed",{reason:e.t("common.reason.no-microphone")}),persistent:!0});return}r.locales.length>0&&!R().length&&v(r.locales),e.toast.show({variant:"success",title:e.t("error.title"),description:e.t("settings.test.success",{locale:r.selected||r.locales[0]||c.value.locale})})}catch(r){e.toast.show({variant:"error",title:e.t("error.title"),description:r instanceof Error?r.message:String(r)})}finally{I(!1)}};return o.createComponent(i.SettingsLayout,{get title(){return e.t("settings.title")},get description(){return e.t("settings.description")},get children(){return[o.createComponent(i.SettingsSection,{get title(){return e.t("settings.section")},get children(){return[o.createComponent(i.SettingsRow,{id:"engine",get title(){return e.t("settings.engine")},get description(){return e.t("settings.engine.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("settings.engine")},get value(){return u.value.mode},get options(){return l()},onChange:r=>G(r)})}}),o.createComponent(y.Show,{get when(){return u.value.mode==="windows"},get children(){return o.createComponent(i.SettingsRow,{id:"winh",get title(){return e.t("settings.winh")},get description(){return e.t("settings.winh.description")},get children(){return o.createComponent(i.Button,{onClick:()=>{X()},get children(){return e.t("settings.winh.run")}})}})}}),o.createComponent(y.Show,{get when(){return u.value.mode==="whisper"},get children(){return[o.createComponent(i.SettingsRow,{id:"whisper-python",get title(){return e.t("whisper.python")},get description(){return e.t("whisper.python.description")},get children(){return o.createComponent(i.TextInput,{get label(){return e.t("whisper.python")},get value(){return a.value.python},onInput:r=>J({python:r})})}}),o.createComponent(i.SettingsRow,{id:"whisper-model",get title(){return e.t("whisper.model")},get description(){return e.t("whisper.model.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("whisper.model")},get value(){return a.value.model},options:[{value:"large-v3-turbo",label:"large-v3-turbo"},{value:"large-v3",label:"large-v3"},{value:"medium",label:"medium"}],onChange:r=>J({model:r})})}}),o.createComponent(i.SettingsRow,{id:"whisper-language",get title(){return e.t("whisper.language")},get description(){return e.t("whisper.language.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("whisper.language")},get value(){return a.value.language},get options(){return[{value:"",label:e.t("whisper.language.auto")},{value:"es",label:"Español"},{value:"en",label:"English"},{value:"pt",label:"Português"}]},onChange:r=>J({language:r})})}}),o.createComponent(i.SettingsRow,{id:"whisper-test",get title(){return e.t("whisper.test")},get description(){return e.t("whisper.test.description")},get children(){return o.createComponent(i.Button,{get busy(){return z()},onClick:()=>{k()},get children(){return e.t("whisper.test.run")}})}})]}}),o.createComponent(y.Show,{get when(){return u.value.mode==="whispercpp"},get children(){return[o.createComponent(i.SettingsRow,{id:"whispercpp-model",get title(){return e.t("whispercpp.model")},get description(){return e.t("whispercpp.model.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("whispercpp.model")},get value(){return g.value.model},options:[{value:"base-q5_1",label:"base-q5_1 · 57 MB"},{value:"small-q5_1",label:"small-q5_1 · 180 MB"}],onChange:r=>se({model:r})})}}),o.createComponent(i.SettingsRow,{id:"whispercpp-language",get title(){return e.t("whispercpp.language")},get description(){return e.t("whispercpp.language.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("whispercpp.language")},get value(){return g.value.language},get options(){return[{value:"auto",label:e.t("whispercpp.language.auto")},{value:"es",label:"Español"},{value:"en",label:"English"},{value:"pt",label:"Português"}]},onChange:r=>se({language:r})})}}),o.createComponent(i.SettingsRow,{id:"whispercpp-ffmpeg",get title(){return e.t("whispercpp.ffmpeg")},get description(){return H()},get children(){return[o.createComponent(y.Show,{get when(){return F()?.available===!1},get children(){return o.createComponent(i.Button,{get busy(){return Z()},onClick:()=>{f()},get children(){return e.t("whispercpp.ffmpeg.install")}})}}),o.createComponent(y.Show,{get when(){return F()?.available!==!1},get children(){return o.createComponent(i.Button,{onClick:()=>{_().catch(()=>{})},get children(){return e.t("whispercpp.test.run")}})}})]}}),o.createComponent(i.SettingsRow,{id:"whispercpp-install",get title(){return e.t("whispercpp.install")},get description(){return e.t("whispercpp.install.description")},get children(){return o.createComponent(i.Button,{get busy(){return N()},onClick:()=>{pe()},get children(){return e.t("whispercpp.install.run")}})}}),o.createComponent(i.SettingsRow,{id:"whispercpp-test",get title(){return e.t("whispercpp.test")},get description(){return e.t("whispercpp.test.description")},get children(){return o.createComponent(i.Button,{get busy(){return A()},onClick:()=>{w()},get children(){return e.t("whispercpp.test.run")}})}})]}}),o.createComponent(y.Show,{get when(){return u.value.mode==="system"},get children(){return[o.createComponent(i.SettingsRow,{id:"language",get title(){return e.t("settings.language")},get description(){return e.t("settings.language.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("settings.language")},get value(){return c.value.locale},get options(){return o.memo(()=>m().length>0)()?m():Ue.map(r=>({value:r,label:r}))},onChange:r=>M({locale:r})})}}),o.createComponent(i.SettingsRow,{id:"confidence",get title(){return e.t("settings.confidence")},get description(){return e.t("settings.confidence.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("settings.confidence")},get value(){return String(c.value.minConfidence)},get options(){return[{value:"0.2",label:e.t("confidence.low")},{value:"0.4",label:e.t("confidence.normal")},{value:"0.6",label:e.t("confidence.high")}]},onChange:r=>M({minConfidence:Number(r)||.4})})}}),o.createComponent(i.SettingsRow,{id:"space",get title(){return e.t("settings.space")},get description(){return e.t("settings.space.description")},get children(){return o.createComponent(i.Switch,{get label(){return e.t("settings.space")},hideLabel:!0,get checked(){return c.value.trailingSpace},onChange:r=>M({trailingSpace:r})})}}),o.createComponent(i.SettingsRow,{id:"focus",get title(){return e.t("settings.focus")},get description(){return e.t("settings.focus.description")},get children(){return o.createComponent(i.Switch,{get label(){return e.t("settings.focus")},hideLabel:!0,get checked(){return c.value.autoFocus},onChange:r=>M({autoFocus:r})})}})]}})]}}),o.createComponent(i.SettingsSection,{get title(){return e.t("speech.section")},get children(){return[o.createComponent(i.SettingsRow,{id:"tts-engine",get title(){return e.t("speech.engine")},get description(){return e.t("speech.engine.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("speech.engine")},get value(){return s.value.engine},get options(){return[{value:"edge",label:e.t("speech.engine.edge")},{value:"system",label:e.t("speech.engine.system")}]},onChange:r=>P(r)})}}),o.createComponent(i.SettingsRow,{id:"voice",get title(){return e.t("speech.voice")},get description(){return e.t("speech.voice.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("speech.voice")},get value(){return E()},get options(){return b()},onChange:r=>ee(r)})}}),o.createComponent(y.Show,{get when(){return t.speaker.error()},get children(){return o.createComponent(i.SettingsRow,{id:"tts-error",get title(){return e.t("speech.edge.error")},get description(){return t.speaker.error()}})}}),o.createComponent(i.SettingsRow,{id:"rate",get title(){return e.t("speech.rate")},get description(){return e.t("speech.rate.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("speech.rate")},get value(){return String(n.value.rate)},get options(){return[{value:"0.8",label:e.t("speech.rate.slow")},{value:"1",label:e.t("speech.rate.normal")},{value:"1.2",label:e.t("speech.rate.fast")},{value:"1.5",label:e.t("speech.rate.faster")}]},onChange:r=>O({rate:Number(r)||1})})}}),o.createComponent(i.SettingsRow,{id:"skip-code",get title(){return e.t("speech.skipCode")},get description(){return e.t("speech.skipCode.description")},get children(){return o.createComponent(i.Switch,{get label(){return e.t("speech.skipCode")},hideLabel:!0,get checked(){return n.value.skipCode},onChange:r=>O({skipCode:r})})}}),o.createComponent(i.SettingsRow,{id:"preview",get title(){return e.t("speech.preview")},get description(){return e.t("speech.preview.description")},get children(){return o.createComponent(i.Button,{onClick:()=>{Y()},get children(){return e.t("read.idle")}})}})]}}),o.createComponent(y.Show,{get when(){return W()!=="linux"},get children(){return o.createComponent(i.SettingsSection,{get title(){return e.t("settings.test.section")},get children(){return o.createComponent(i.SettingsRow,{id:"test",get title(){return e.t("settings.test")},get description(){return e.t("settings.test.description")},get children(){return o.createComponent(i.Button,{get busy(){return C()},onClick:()=>{V()},get children(){return e.t("settings.test.run")}})}})}})}})]}})}async function He(){const t=await navigator.mediaDevices.getUserMedia({audio:{channelCount:1,echoCancellation:!0,noiseSuppression:!0}}),e=MediaRecorder.isTypeSupported("audio/webm;codecs=opus")?"audio/webm;codecs=opus":"audio/webm",i=new MediaRecorder(t,{mimeType:e}),c=[];return i.ondataavailable=n=>{n.data.size>0&&c.push(n.data)},i.start(),{stop:()=>new Promise(n=>{i.onstop=()=>{t.getTracks().forEach(s=>s.stop()),n(new Blob(c,{type:e}))},i.stop()})}}async function Me(t){const e=new Uint8Array(await t.arrayBuffer());let i="";const c=32768;for(let n=0;n<e.length;n+=c)i+=String.fromCharCode(...e.subarray(n,n+c));return btoa(i)}function Ge(t){const e=$.usePlugin();return o.memo(()=>t.actions().map(i=>o.createComponent(e.ui.Button,{get variant(){return i.id==="stop"?"danger":"neutral"},get icon(){return i.icon},onClick:()=>{i.run(),t.close()},get children(){return i.label}})))}const Xe=t=>{const e=t.main(le),i=t.main(fe),c=t.main(ce),n=Be(t,i),s=t.stores.prefs,u=t.stores.engine,a=t.stores.whisper,g=t.stores.whispercpp,[h,S]=y.createSignal(!1),[R,v]=y.createSignal("idle");t.client.rpc(j).capabilities({}).then(l=>{const m=l;!m.engines.includes(u.value.mode)&&m.engines.length>0&&u.update(b=>{b.mode=m.engines[0]??"windows"})}).catch(()=>{});const C=()=>u.value.mode==="whisper"||u.value.mode==="whispercpp"?u.value.mode:void 0;let I,z;const B=(l,m,b)=>{switch(l){case"LANGUAGE_NOT_AVAILABLE":return t.t("error.language",{locale:m||s.value.locale,available:b.length>0?b.join(", "):"—"});case"NO_RECOGNIZERS":return t.t("error.none");case"MICROPHONE_UNAVAILABLE":return t.t("error.mic");case"SYSTEM_SPEECH_UNAVAILABLE":return t.t("error.system");case"ENGINE_EXIT":return t.t("error.exit");default:return t.t("error.unknown",{code:l})}},N=()=>{h()&&(S(!1),c.toggle().catch(()=>{})),n.cancel(),I=t.sessions.current()?.key,e.start({locale:s.value.locale,minConfidence:s.value.minConfidence}).catch(l=>{t.toast.show({variant:"error",title:t.t("error.title"),description:l instanceof Error?l.message:String(l)})})},U=()=>{e.stop().catch(()=>{})},A=()=>{n.cancel(),t.screen.current()?.composer.focus(),c.toggle().then(()=>S(l=>!l)).catch(l=>{S(!1),t.toast.show({variant:"error",title:t.t("pill.title"),description:l instanceof Error?`${t.t("error.winh")} ${l.message}`:t.t("error.winh")})})},q=()=>t.t(C()==="whispercpp"?"error.whispercpp":"error.whisper"),Z=async()=>{if(R()==="idle"){n.cancel();try{z=await He(),v("recording")}catch(l){t.toast.show({variant:"error",title:t.t("pill.title"),description:te(l,q())})}}},L=()=>{const l=z;z=void 0,l&&l.stop().catch(()=>{}),v("idle")},F=async()=>{const l=z;if(z=void 0,!l){v("idle");return}v("transcribing");const m=C();try{const b=await l.stop(),E=await Me(b),M=(m==="whispercpp"?await t.client.rpc(j).whisperCppTranscribe({audio:E,mime:b.type,language:g.value.language,model:g.value.model}):await t.client.rpc(j).whisperTranscribe({audio:E,mime:b.type,language:a.value.language,python:a.value.python,model:a.value.model})).text.trim();if(!M){t.toast.show({variant:"default",title:t.t("pill.title"),description:t.t(m==="whispercpp"?"whispercpp.empty":"whisper.empty")});return}const O=t.screen.current()?.composer;if(O){const G=O.text(),J=G.length>0&&!/\s$/.test(G)?" ":"";O.insert(J+M),O.focus()}}catch(b){const E=te(b,q());t.toast.show({variant:"error",title:t.t("pill.title"),description:E.includes("FFMPEG_MISSING")?t.t("error.ffmpeg"):E})}finally{v("idle")}},T=()=>{if(C()){R()==="recording"?F():R()==="idle"&&Z();return}if(u.value.mode==="windows"){A();return}const l=e.state()?.status??"idle";l==="starting"||l==="listening"?U():N()},W=()=>{if(R()==="recording"&&L(),n.speaking()){n.cancel();return}if(!t.sessions.current()){t.toast.show({variant:"error",title:t.t("read.title"),description:t.t("read.noSession")});return}const l=e.state()?.status??"idle";(l==="starting"||l==="listening")&&e.stop().catch(()=>{}),(async()=>{try{const m=await n.lastAssistantText();if(!m.trim()){t.toast.show({variant:"default",title:t.t("read.title"),description:t.t("read.empty")});return}await n.speak(m)||t.toast.show({variant:"default",title:t.t("read.title"),description:t.t("read.empty")})}catch(m){t.toast.show({variant:"error",title:t.t("read.title"),description:m instanceof Error?m.message:t.t("read.error")})}})()},x=()=>{if(!n.speaking())return[{id:"start",label:t.t("read.idle"),icon:"outline-audio",run:W}];const l=n.paused(),m=[{id:"restart",label:t.t("read.restart"),icon:"outline-reset",run:()=>{n.restart()}}];return n.canPause()&&m.push({id:"pause",label:l?t.t("read.resume"):t.t("read.pause"),icon:"outline-audio",run:()=>n.togglePause()}),m.push({id:"stop",label:t.t("read.stop"),icon:"xmark-small",run:()=>n.cancel()}),m};e.on("final",l=>{const m=t.sessions.current()?.key;if(I&&m&&m!==I){t.toast.show({variant:"error",title:t.t("error.title"),description:t.t("toast.discarded")});return}const b=t.screen.current()?.composer;if(!b)return;const E=b.text(),ee=s.value.trailingSpace&&E.length>0&&!/\s$/.test(E)?" ":"";b.insert(ee+l.text),s.value.autoFocus&&b.focus()}),e.on("error",l=>{t.toast.show({variant:"error",title:t.t("error.title"),description:B(l.code,l.detail,l.available),persistent:l.code==="LANGUAGE_NOT_AVAILABLE"||l.code==="NO_RECOGNIZERS",duration:8e3})});const _=()=>{if(t.app.platform!=="desktop")return;if(u.value.mode==="windows")return h()?{label:t.t("pill.dictating"),icon:"outline-wave-audio",tone:"danger",title:t.t("pill.stop"),run:T}:{label:t.t("pill.idle"),icon:"outline-wave-audio",title:t.t("pill.winH"),run:T};if(u.value.mode==="whisper"||u.value.mode==="whispercpp"){const m=t.t(u.value.mode==="whispercpp"?"pill.whispercpp":"pill.whisper");return R()==="recording"?{label:t.t("pill.recording"),icon:"outline-wave-audio",tone:"danger",title:t.t("pill.stop"),run:T}:R()==="transcribing"?{label:t.t("pill.transcribing"),icon:"outline-audio",title:m}:{label:t.t("pill.idle"),icon:"outline-wave-audio",title:m,run:T}}const l=e.state()?.status??"idle";return l==="starting"?{label:t.t("pill.starting"),icon:"outline-wave-audio",title:t.t("pill.title"),run:T}:l==="listening"?{label:t.t("pill.listening"),icon:"outline-wave-audio",tone:"danger",title:t.t("pill.stop"),run:T}:l==="error"?{label:t.t("pill.error"),icon:"outline-wave-audio",tone:"warning",title:t.t("pill.title"),run:T}:{label:t.t("pill.idle"),icon:"outline-wave-audio",title:t.t("pill.title"),run:T}},H=()=>{if(t.app.platform!=="desktop")return;const l=n.speaking(),m=n.paused(),b=l?x().map(E=>({id:E.id,label:E.label,icon:E.icon,tone:E.id==="stop"?"danger":void 0,run:E.run})):[];return{label:l?m?t.t("read.paused"):t.t("read.speaking"):t.t("read.idle"),icon:"outline-audio",tone:l?"success":void 0,title:t.t("read.title"),rows:b.length>0?b:void 0,run:l?void 0:W}};t.add($.ComposerPill,()=>({id:"dictate",show:()=>_()})),t.add($.ComposerPill,()=>({id:"read",order:1,show:()=>H(),menu:l=>o.createComponent(Ge,{actions:x,get close(){return l.close}})})),t.add($.Command,()=>({id:"toggle",title:t.t("command.toggle"),description:t.t("command.toggle.description"),keywords:"voice dictation microphone speech dictado voz microfono",group:t.t("pill.title"),section:"prompt",bind:"mod+shift+m",slash:{name:"dictado"},editable:!0,enabled:t.app.platform==="desktop",run:T})),t.add($.Command,()=>({id:"configure",title:t.t("command.configure"),keywords:"voice dictation microphone speech dictado voz microfono",group:t.t("pill.title"),enabled:t.app.platform==="desktop",run:()=>t.layout.settings(t.id)})),t.add($.Command,()=>({id:"read",title:t.t("command.read"),description:t.t("command.read.description"),keywords:"read aloud speech voice leer voz altavoz lectura",group:t.t("pill.title"),section:"prompt",bind:"mod+shift+l",slash:{name:"leer"},editable:!0,enabled:t.app.platform==="desktop",run:W})),t.add($.SettingsPage,{id:t.id,icon:"outline-audio",get title(){return t.t("settings.title")},get entries(){return[{id:"engine",title:t.t("settings.engine"),keywords:"engine motor dictation dictado winh voz"},{id:"winh",title:t.t("settings.winh"),keywords:"winh windows voice typing dictado"},{id:"language",title:t.t("settings.language"),keywords:"voice language idioma voz"},{id:"confidence",title:t.t("settings.confidence"),keywords:"voice confidence confianza"},{id:"test",title:t.t("settings.test"),keywords:"voice microphone microphone test microfono"},{id:"voice",title:t.t("speech.voice"),keywords:"read aloud voice voz lectura altavoz"},{id:"tts-engine",title:t.t("speech.engine"),keywords:"voice engine motor edge neural sistema"},{id:"whisper-python",title:t.t("whisper.python"),keywords:"whisper python venv entorno"},{id:"whisper-model",title:t.t("whisper.model"),keywords:"whisper model modelo"},{id:"whisper-language",title:t.t("whisper.language"),keywords:"whisper language idioma"},{id:"whisper-test",title:t.t("whisper.test"),keywords:"whisper check comprobar"},{id:"whispercpp-model",title:t.t("whispercpp.model"),keywords:"whisper.cpp model modelo"},{id:"whispercpp-language",title:t.t("whispercpp.language"),keywords:"whisper.cpp language idioma"},{id:"whispercpp-ffmpeg",title:t.t("whispercpp.ffmpeg"),keywords:"whisper.cpp ffmpeg audio convertir"},{id:"whispercpp-install",title:t.t("whispercpp.install"),keywords:"whisper.cpp download descargar instalar"},{id:"whispercpp-test",title:t.t("whispercpp.test"),keywords:"whisper.cpp check comprobar"},{id:"rate",title:t.t("speech.rate"),keywords:"read aloud speed velocidad lectura"},{id:"skip-code",title:t.t("speech.skipCode"),keywords:"read aloud code codigo lectura"},{id:"preview",title:t.t("speech.preview"),keywords:"read aloud test voice probar voz"}]},render:()=>o.createComponent(qe,{speaker:n})})},Ke=$.Plugin.define({...$e,setup:Xe,main:Fe});exports.default=Ke;
|
|
192
|
+
`)};return e.on("done",()=>{v(!1),S(!1)}),W()?.addEventListener?.("voiceschanged",()=>{_()}),_(),{speaking:h,paused:R,backend:N,engine:n,voice:u,error:z,canPause:m,setConfig:g,voices:C,refreshVoices:_,lastAssistantText:J,speak:G,cancel:l,pause:b,resume:E,togglePause:ee,restart:M,preview:async()=>{const w=await G(t.t("speech.preview.text"));if(w){const f=n()==="edge"?`Edge · ${u()}`:`${N()} · ${i.value.voiceURI||"auto"}`;t.toast.show({variant:"default",title:t.t("read.title"),description:f})}return w}}}function te(t,e){if(t instanceof Error&&t.message)return t.message;if(typeof t=="string"&&t)return t;if(t&&typeof t=="object"){const i=t;for(const n of["message","error","detail","defect"]){const s=i[n];if(typeof s=="string"&&s)return s}const c=i.data;if(c&&typeof c=="object"){const n=c.message;if(typeof n=="string"&&n)return n}try{const n=JSON.stringify(t);if(n&&n!=="{}")return n}catch{return e}}return e}const Ue=["es-ES","es-MX","es-AR","es-CO","pt-BR","pt-PT","en-US","en-GB","fr-FR","de-DE","it-IT"];function qe(t){const e=$.usePlugin(),i=e.ui,c=e.stores.prefs,n=e.stores.speech,s=e.stores.tts,u=e.stores.engine,a=e.stores.whisper,g=e.stores.whispercpp,h=e.main(le),S=e.main(ce),[R,v]=y.createSignal([]),[C,I]=y.createSignal(!1),[z,B]=y.createSignal(!1),[N,U]=y.createSignal(!1),[A,q]=y.createSignal(!1),[Z,L]=y.createSignal(!1),[F,T]=y.createSignal(void 0),[W,x]=y.createSignal(""),_=async()=>{const r=await e.client.rpc(j).whisperCppStatus({model:g.value.model});return T({available:r.ffmpeg,version:r.ffmpegVersion}),r},H=()=>{const r=F();return r?r.available?e.t("whispercpp.ffmpeg.ready",{version:r.version}):e.t("whispercpp.ffmpeg.missing"):e.t("whispercpp.ffmpeg.description")};y.onMount(()=>{t.speaker.setConfig({engine:s.value.engine,voice:s.value.voice});const r=()=>{h.diagnose().then(d=>{d.locales.length>0&&v(d.locales)}).catch(()=>{})};e.client.rpc(j).capabilities({}).then(d=>{const D=d;x(D.platform),!D.engines.includes(u.value.mode)&&D.engines.length>0&&G(D.engines[0]??"windows"),D.platform!=="linux"?r():_().catch(()=>{})}).catch(()=>r()),t.speaker.refreshVoices()});const l=()=>W()==="linux"?[{value:"whispercpp",label:e.t("engine.whispercpp")}]:[{value:"windows",label:e.t("engine.windows")},{value:"system",label:e.t("engine.system")},{value:"whisper",label:e.t("engine.whisper")}],m=()=>{const r=R(),d=c.value.locale;return(r.includes(d)?r:[d,...r]).map(re=>({value:re,label:re}))},b=()=>{const r=t.speaker.voices().map(d=>({value:d.id,label:d.label}));return s.value.engine==="edge"?r:[{value:"",label:e.t("speech.voice.auto")},...r]},E=()=>s.value.engine==="edge"?s.value.voice:n.value.voiceURI,ee=r=>{if(s.value.engine==="edge"){s.update(d=>{d.voice=r}),t.speaker.setConfig({voice:r});return}O({voiceURI:r})},M=r=>{c.update(d=>{r.locale!==void 0&&(d.locale=r.locale),r.minConfidence!==void 0&&(d.minConfidence=r.minConfidence),r.trailingSpace!==void 0&&(d.trailingSpace=r.trailingSpace),r.autoFocus!==void 0&&(d.autoFocus=r.autoFocus)})},O=r=>{n.update(d=>{r.voiceURI!==void 0&&(d.voiceURI=r.voiceURI),r.rate!==void 0&&(d.rate=r.rate),r.skipCode!==void 0&&(d.skipCode=r.skipCode)})},G=r=>{u.update(d=>{d.mode=r})},J=r=>{a.update(d=>{r.python!==void 0&&(d.python=r.python),r.model!==void 0&&(d.model=r.model),r.language!==void 0&&(d.language=r.language)})},se=r=>{g.update(d=>{r.model!==void 0&&(d.model=r.model),r.language!==void 0&&(d.language=r.language)})},pe=async()=>{U(!0);try{const r=await e.client.rpc(j).whisperCppInstall({model:g.value.model});r.ok?(e.toast.show({variant:"success",title:e.t("pill.title"),description:e.t("whispercpp.install.ok",{model:r.model})}),await _().catch(()=>{})):e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.install.error",{error:r.error||"?"}),persistent:!0})}catch(r){e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.install.error",{error:te(r,"?")}),persistent:!0})}finally{U(!1)}},w=async()=>{q(!0);try{const r=await _();r.error?e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.test.error",{error:r.error}),persistent:!0}):r.installed?r.ffmpeg?e.toast.show({variant:"success",title:e.t("pill.title"),description:e.t("whispercpp.test.ready",{model:r.model})}):e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.ffmpeg.missing"),persistent:!0}):e.toast.show({variant:"default",title:e.t("pill.title"),description:e.t("whispercpp.test.missing")})}catch(r){e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.test.error",{error:te(r,"?")})})}finally{q(!1)}},f=async()=>{L(!0);try{const r=await e.client.rpc(j).whisperCppInstallFfmpeg({});if(r.ok)e.toast.show({variant:"success",title:e.t("pill.title"),description:e.t("whispercpp.ffmpeg.install.ok")}),await _().catch(()=>{});else{const d=r.command?` ${e.t("whispercpp.ffmpeg.install.manual",{command:r.command})}`:"";e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.ffmpeg.install.error",{error:r.error||"?"})+d,persistent:!0})}}catch(r){e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whispercpp.ffmpeg.install.error",{error:te(r,"?")}),persistent:!0})}finally{L(!1)}},k=async()=>{B(!0);try{const r=await e.client.rpc(j).whisperStatus({python:a.value.python,model:a.value.model});r.ready?e.toast.show({variant:"success",title:e.t("pill.title"),description:e.t("whisper.test.ready",{model:r.model,device:r.device})}):e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whisper.test.error",{error:r.error||"?"})})}catch(r){e.toast.show({variant:"error",title:e.t("pill.title"),description:e.t("whisper.test.error",{error:r instanceof Error?r.message:String(r)})})}finally{B(!1)}},P=r=>{s.update(d=>{d.engine=r}),t.speaker.setConfig({engine:r}),t.speaker.refreshVoices()},X=async()=>{try{await S.toggle(),e.toast.show({variant:"default",title:e.t("settings.winh"),description:e.t("settings.winh.sent")})}catch(r){e.toast.show({variant:"error",title:e.t("pill.title"),description:r instanceof Error?r.message:e.t("error.winh")})}},Y=async()=>{try{await t.speaker.preview()}catch(r){e.toast.show({variant:"error",title:e.t("read.title"),description:r instanceof Error?r.message:e.t("read.error")})}},V=async()=>{I(!0);try{const r=await h.diagnose();if(r.locales.length===0){e.toast.show({variant:"error",title:e.t("error.title"),description:e.t("settings.test.failed",{reason:e.t("common.reason.no-recognizers")}),persistent:!0});return}if(!r.microphone){e.toast.show({variant:"error",title:e.t("error.title"),description:e.t("settings.test.failed",{reason:e.t("common.reason.no-microphone")}),persistent:!0});return}r.locales.length>0&&!R().length&&v(r.locales),e.toast.show({variant:"success",title:e.t("error.title"),description:e.t("settings.test.success",{locale:r.selected||r.locales[0]||c.value.locale})})}catch(r){e.toast.show({variant:"error",title:e.t("error.title"),description:r instanceof Error?r.message:String(r)})}finally{I(!1)}};return o.createComponent(i.SettingsLayout,{get title(){return e.t("settings.title")},get description(){return e.t("settings.description")},get children(){return[o.createComponent(i.SettingsSection,{get title(){return e.t("settings.section")},get children(){return[o.createComponent(i.SettingsRow,{id:"engine",get title(){return e.t("settings.engine")},get description(){return e.t("settings.engine.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("settings.engine")},get value(){return u.value.mode},get options(){return l()},onChange:r=>G(r)})}}),o.createComponent(y.Show,{get when(){return u.value.mode==="windows"},get children(){return o.createComponent(i.SettingsRow,{id:"winh",get title(){return e.t("settings.winh")},get description(){return e.t("settings.winh.description")},get children(){return o.createComponent(i.Button,{onClick:()=>{X()},get children(){return e.t("settings.winh.run")}})}})}}),o.createComponent(y.Show,{get when(){return u.value.mode==="whisper"},get children(){return[o.createComponent(i.SettingsRow,{id:"whisper-python",get title(){return e.t("whisper.python")},get description(){return e.t("whisper.python.description")},get children(){return o.createComponent(i.TextInput,{get label(){return e.t("whisper.python")},get value(){return a.value.python},onInput:r=>J({python:r})})}}),o.createComponent(i.SettingsRow,{id:"whisper-model",get title(){return e.t("whisper.model")},get description(){return e.t("whisper.model.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("whisper.model")},get value(){return a.value.model},options:[{value:"large-v3-turbo",label:"large-v3-turbo"},{value:"large-v3",label:"large-v3"},{value:"medium",label:"medium"}],onChange:r=>J({model:r})})}}),o.createComponent(i.SettingsRow,{id:"whisper-language",get title(){return e.t("whisper.language")},get description(){return e.t("whisper.language.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("whisper.language")},get value(){return a.value.language},get options(){return[{value:"",label:e.t("whisper.language.auto")},{value:"es",label:"Español"},{value:"en",label:"English"},{value:"pt",label:"Português"}]},onChange:r=>J({language:r})})}}),o.createComponent(i.SettingsRow,{id:"whisper-test",get title(){return e.t("whisper.test")},get description(){return e.t("whisper.test.description")},get children(){return o.createComponent(i.Button,{get busy(){return z()},onClick:()=>{k()},get children(){return e.t("whisper.test.run")}})}})]}}),o.createComponent(y.Show,{get when(){return u.value.mode==="whispercpp"},get children(){return[o.createComponent(i.SettingsRow,{id:"whispercpp-model",get title(){return e.t("whispercpp.model")},get description(){return e.t("whispercpp.model.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("whispercpp.model")},get value(){return g.value.model},options:[{value:"base-q5_1",label:"base-q5_1 · 57 MB"},{value:"small-q5_1",label:"small-q5_1 · 180 MB"}],onChange:r=>se({model:r})})}}),o.createComponent(i.SettingsRow,{id:"whispercpp-language",get title(){return e.t("whispercpp.language")},get description(){return e.t("whispercpp.language.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("whispercpp.language")},get value(){return g.value.language},get options(){return[{value:"auto",label:e.t("whispercpp.language.auto")},{value:"es",label:"Español"},{value:"en",label:"English"},{value:"pt",label:"Português"}]},onChange:r=>se({language:r})})}}),o.createComponent(i.SettingsRow,{id:"whispercpp-ffmpeg",get title(){return e.t("whispercpp.ffmpeg")},get description(){return H()},get children(){return[o.createComponent(y.Show,{get when(){return F()?.available===!1},get children(){return o.createComponent(i.Button,{get busy(){return Z()},onClick:()=>{f()},get children(){return e.t("whispercpp.ffmpeg.install")}})}}),o.createComponent(y.Show,{get when(){return F()?.available!==!1},get children(){return o.createComponent(i.Button,{onClick:()=>{_().catch(()=>{})},get children(){return e.t("whispercpp.test.run")}})}})]}}),o.createComponent(i.SettingsRow,{id:"whispercpp-install",get title(){return e.t("whispercpp.install")},get description(){return e.t("whispercpp.install.description")},get children(){return o.createComponent(i.Button,{get busy(){return N()},onClick:()=>{pe()},get children(){return e.t("whispercpp.install.run")}})}}),o.createComponent(i.SettingsRow,{id:"whispercpp-test",get title(){return e.t("whispercpp.test")},get description(){return e.t("whispercpp.test.description")},get children(){return o.createComponent(i.Button,{get busy(){return A()},onClick:()=>{w()},get children(){return e.t("whispercpp.test.run")}})}})]}}),o.createComponent(y.Show,{get when(){return u.value.mode==="system"},get children(){return[o.createComponent(i.SettingsRow,{id:"language",get title(){return e.t("settings.language")},get description(){return e.t("settings.language.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("settings.language")},get value(){return c.value.locale},get options(){return o.memo(()=>m().length>0)()?m():Ue.map(r=>({value:r,label:r}))},onChange:r=>M({locale:r})})}}),o.createComponent(i.SettingsRow,{id:"confidence",get title(){return e.t("settings.confidence")},get description(){return e.t("settings.confidence.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("settings.confidence")},get value(){return String(c.value.minConfidence)},get options(){return[{value:"0.2",label:e.t("confidence.low")},{value:"0.4",label:e.t("confidence.normal")},{value:"0.6",label:e.t("confidence.high")}]},onChange:r=>M({minConfidence:Number(r)||.4})})}}),o.createComponent(i.SettingsRow,{id:"space",get title(){return e.t("settings.space")},get description(){return e.t("settings.space.description")},get children(){return o.createComponent(i.Switch,{get label(){return e.t("settings.space")},hideLabel:!0,get checked(){return c.value.trailingSpace},onChange:r=>M({trailingSpace:r})})}}),o.createComponent(i.SettingsRow,{id:"focus",get title(){return e.t("settings.focus")},get description(){return e.t("settings.focus.description")},get children(){return o.createComponent(i.Switch,{get label(){return e.t("settings.focus")},hideLabel:!0,get checked(){return c.value.autoFocus},onChange:r=>M({autoFocus:r})})}})]}})]}}),o.createComponent(i.SettingsSection,{get title(){return e.t("speech.section")},get children(){return[o.createComponent(i.SettingsRow,{id:"tts-engine",get title(){return e.t("speech.engine")},get description(){return e.t("speech.engine.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("speech.engine")},get value(){return s.value.engine},get options(){return[{value:"edge",label:e.t("speech.engine.edge")},{value:"system",label:e.t("speech.engine.system")}]},onChange:r=>P(r)})}}),o.createComponent(i.SettingsRow,{id:"voice",get title(){return e.t("speech.voice")},get description(){return e.t("speech.voice.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("speech.voice")},get value(){return E()},get options(){return b()},onChange:r=>ee(r)})}}),o.createComponent(y.Show,{get when(){return t.speaker.error()},get children(){return o.createComponent(i.SettingsRow,{id:"tts-error",get title(){return e.t("speech.edge.error")},get description(){return t.speaker.error()}})}}),o.createComponent(i.SettingsRow,{id:"rate",get title(){return e.t("speech.rate")},get description(){return e.t("speech.rate.description")},get children(){return o.createComponent(i.Select,{get label(){return e.t("speech.rate")},get value(){return String(n.value.rate)},get options(){return[{value:"0.8",label:e.t("speech.rate.slow")},{value:"1",label:e.t("speech.rate.normal")},{value:"1.2",label:e.t("speech.rate.fast")},{value:"1.5",label:e.t("speech.rate.faster")}]},onChange:r=>O({rate:Number(r)||1})})}}),o.createComponent(i.SettingsRow,{id:"skip-code",get title(){return e.t("speech.skipCode")},get description(){return e.t("speech.skipCode.description")},get children(){return o.createComponent(i.Switch,{get label(){return e.t("speech.skipCode")},hideLabel:!0,get checked(){return n.value.skipCode},onChange:r=>O({skipCode:r})})}}),o.createComponent(i.SettingsRow,{id:"preview",get title(){return e.t("speech.preview")},get description(){return e.t("speech.preview.description")},get children(){return o.createComponent(i.Button,{onClick:()=>{Y()},get children(){return e.t("read.idle")}})}})]}}),o.createComponent(y.Show,{get when(){return W()!=="linux"},get children(){return o.createComponent(i.SettingsSection,{get title(){return e.t("settings.test.section")},get children(){return o.createComponent(i.SettingsRow,{id:"test",get title(){return e.t("settings.test")},get description(){return e.t("settings.test.description")},get children(){return o.createComponent(i.Button,{get busy(){return C()},onClick:()=>{V()},get children(){return e.t("settings.test.run")}})}})}})}})]}})}async function He(){const t=await navigator.mediaDevices.getUserMedia({audio:{channelCount:1,echoCancellation:!0,noiseSuppression:!0}}),e=MediaRecorder.isTypeSupported("audio/webm;codecs=opus")?"audio/webm;codecs=opus":"audio/webm",i=new MediaRecorder(t,{mimeType:e}),c=[];return i.ondataavailable=n=>{n.data.size>0&&c.push(n.data)},i.start(),{stop:()=>new Promise(n=>{i.onstop=()=>{t.getTracks().forEach(s=>s.stop()),n(new Blob(c,{type:e}))},i.stop()})}}async function Me(t){const e=new Uint8Array(await t.arrayBuffer());let i="";const c=32768;for(let n=0;n<e.length;n+=c)i+=String.fromCharCode(...e.subarray(n,n+c));return btoa(i)}function Ge(t){const e=$.usePlugin();return o.memo(()=>t.actions().map(i=>o.createComponent(e.ui.Button,{get variant(){return i.id==="stop"?"danger":"neutral"},get icon(){return i.icon},onClick:()=>{i.run(),t.close()},get children(){return i.label}})))}const Xe=t=>{const e=t.main(le),i=t.main(fe),c=t.main(ce),n=Be(t,i),s=t.stores.prefs,u=t.stores.engine,a=t.stores.whisper,g=t.stores.whispercpp,[h,S]=y.createSignal(!1),[R,v]=y.createSignal("idle");t.client.rpc(j).capabilities({}).then(l=>{const m=l;!m.engines.includes(u.value.mode)&&m.engines.length>0&&u.update(b=>{b.mode=m.engines[0]??"windows"})}).catch(()=>{});const C=()=>u.value.mode==="whisper"||u.value.mode==="whispercpp"?u.value.mode:void 0;let I,z;const B=(l,m,b)=>{switch(l){case"LANGUAGE_NOT_AVAILABLE":return t.t("error.language",{locale:m||s.value.locale,available:b.length>0?b.join(", "):"—"});case"NO_RECOGNIZERS":return t.t("error.none");case"MICROPHONE_UNAVAILABLE":return t.t("error.mic");case"SYSTEM_SPEECH_UNAVAILABLE":return t.t("error.system");case"ENGINE_EXIT":return t.t("error.exit");default:return t.t("error.unknown",{code:l})}},N=()=>{h()&&(S(!1),c.toggle().catch(()=>{})),n.cancel(),I=t.sessions.current()?.key,e.start({locale:s.value.locale,minConfidence:s.value.minConfidence}).catch(l=>{t.toast.show({variant:"error",title:t.t("error.title"),description:l instanceof Error?l.message:String(l)})})},U=()=>{e.stop().catch(()=>{})},A=()=>{n.cancel(),t.screen.current()?.composer.focus(),c.toggle().then(()=>S(l=>!l)).catch(l=>{S(!1),t.toast.show({variant:"error",title:t.t("pill.title"),description:l instanceof Error?`${t.t("error.winh")} ${l.message}`:t.t("error.winh")})})},q=()=>t.t(C()==="whispercpp"?"error.whispercpp":"error.whisper"),Z=async()=>{if(R()==="idle"){n.cancel();try{z=await He(),v("recording")}catch(l){t.toast.show({variant:"error",title:t.t("pill.title"),description:te(l,q())})}}},L=()=>{const l=z;z=void 0,l&&l.stop().catch(()=>{}),v("idle")},F=async()=>{const l=z;if(z=void 0,!l){v("idle");return}v("transcribing");const m=C();try{const b=await l.stop(),E=await Me(b),M=(m==="whispercpp"?await t.client.rpc(j).whisperCppTranscribe({audio:E,mime:b.type,language:g.value.language,model:g.value.model}):await t.client.rpc(j).whisperTranscribe({audio:E,mime:b.type,language:a.value.language,python:a.value.python,model:a.value.model})).text.trim();if(!M){t.toast.show({variant:"default",title:t.t("pill.title"),description:t.t(m==="whispercpp"?"whispercpp.empty":"whisper.empty")});return}const O=t.screen.current()?.composer;if(O){const G=O.text(),J=G.length>0&&!/\s$/.test(G)?" ":"";O.insert(J+M),O.focus()}}catch(b){const E=te(b,q());t.toast.show({variant:"error",title:t.t("pill.title"),description:E.includes("FFMPEG_MISSING")?t.t("error.ffmpeg"):E})}finally{v("idle")}},T=()=>{if(C()){R()==="recording"?F():R()==="idle"&&Z();return}if(u.value.mode==="windows"){A();return}const l=e.state()?.status??"idle";l==="starting"||l==="listening"?U():N()},W=()=>{if(R()==="recording"&&L(),n.speaking()){n.cancel();return}if(!t.sessions.current()){t.toast.show({variant:"error",title:t.t("read.title"),description:t.t("read.noSession")});return}const l=e.state()?.status??"idle";(l==="starting"||l==="listening")&&e.stop().catch(()=>{}),(async()=>{try{const m=await n.lastAssistantText();if(!m.trim()){t.toast.show({variant:"default",title:t.t("read.title"),description:t.t("read.empty")});return}await n.speak(m)||t.toast.show({variant:"default",title:t.t("read.title"),description:t.t("read.empty")})}catch(m){t.toast.show({variant:"error",title:t.t("read.title"),description:m instanceof Error?m.message:t.t("read.error")})}})()},x=()=>{if(!n.speaking())return[{id:"start",label:t.t("read.idle"),icon:"outline-audio",run:W}];const l=n.paused(),m=[{id:"restart",label:t.t("read.restart"),icon:"outline-reset",run:()=>{n.restart()}}];return n.canPause()&&m.push({id:"pause",label:l?t.t("read.resume"):t.t("read.pause"),icon:"outline-audio",run:()=>n.togglePause()}),m.push({id:"stop",label:t.t("read.stop"),icon:"xmark-small",run:()=>n.cancel()}),m};e.on("final",l=>{const m=t.sessions.current()?.key;if(I&&m&&m!==I){t.toast.show({variant:"error",title:t.t("error.title"),description:t.t("toast.discarded")});return}const b=t.screen.current()?.composer;if(!b)return;const E=b.text(),ee=s.value.trailingSpace&&E.length>0&&!/\s$/.test(E)?" ":"";b.insert(ee+l.text),s.value.autoFocus&&b.focus()}),e.on("error",l=>{t.toast.show({variant:"error",title:t.t("error.title"),description:B(l.code,l.detail,l.available),persistent:l.code==="LANGUAGE_NOT_AVAILABLE"||l.code==="NO_RECOGNIZERS",duration:8e3})});const _=()=>{if(t.app.platform!=="desktop")return;if(u.value.mode==="windows")return h()?{label:t.t("pill.dictating"),icon:"outline-wave-audio",tone:"danger",title:t.t("pill.stop"),run:T}:{label:t.t("pill.idle"),icon:"outline-wave-audio",title:t.t("pill.winH"),run:T};if(u.value.mode==="whisper"||u.value.mode==="whispercpp"){const m=t.t(u.value.mode==="whispercpp"?"pill.whispercpp":"pill.whisper");return R()==="recording"?{label:t.t("pill.recording"),icon:"outline-wave-audio",tone:"danger",title:t.t("pill.stop"),run:T}:R()==="transcribing"?{label:t.t("pill.transcribing"),icon:"outline-audio",title:m}:{label:t.t("pill.idle"),icon:"outline-wave-audio",title:m,run:T}}const l=e.state()?.status??"idle";return l==="starting"?{label:t.t("pill.starting"),icon:"outline-wave-audio",title:t.t("pill.title"),run:T}:l==="listening"?{label:t.t("pill.listening"),icon:"outline-wave-audio",tone:"danger",title:t.t("pill.stop"),run:T}:l==="error"?{label:t.t("pill.error"),icon:"outline-wave-audio",tone:"warning",title:t.t("pill.title"),run:T}:{label:t.t("pill.idle"),icon:"outline-wave-audio",title:t.t("pill.title"),run:T}},H=()=>{if(t.app.platform!=="desktop")return;const l=n.speaking(),m=n.paused(),b=l?x().map(E=>({id:E.id,label:E.label,icon:E.icon,tone:E.id==="stop"?"danger":void 0,run:E.run})):[];return{label:l?m?t.t("read.paused"):t.t("read.speaking"):t.t("read.idle"),icon:"outline-audio",tone:l?"success":void 0,title:t.t("read.title"),rows:b.length>0?b:void 0,run:l?void 0:W}};t.add($.ComposerPill,()=>({id:"dictate",show:()=>_()})),t.add($.ComposerPill,()=>({id:"read",order:1,show:()=>H(),menu:l=>o.createComponent(Ge,{actions:x,get close(){return l.close}})})),t.add($.Command,()=>({id:"toggle",title:t.t("command.toggle"),description:t.t("command.toggle.description"),keywords:"voice dictation microphone speech dictado voz microfono",group:t.t("pill.title"),section:"prompt",bind:"mod+shift+m",slash:{name:"dictado"},editable:!0,enabled:t.app.platform==="desktop",run:T})),t.add($.Command,()=>({id:"configure",title:t.t("command.configure"),keywords:"voice dictation microphone speech dictado voz microfono",group:t.t("pill.title"),enabled:t.app.platform==="desktop",run:()=>t.layout.settings(t.id)})),t.add($.Command,()=>({id:"read",title:t.t("command.read"),description:t.t("command.read.description"),keywords:"read aloud speech voice leer voz altavoz lectura",group:t.t("pill.title"),section:"prompt",bind:"mod+shift+l",slash:{name:"leer"},editable:!0,enabled:t.app.platform==="desktop",run:W})),t.add($.SettingsPage,{id:t.id,icon:"outline-audio",get title(){return t.t("settings.title")},get entries(){return[{id:"engine",title:t.t("settings.engine"),keywords:"engine motor dictation dictado winh voz"},{id:"winh",title:t.t("settings.winh"),keywords:"winh windows voice typing dictado"},{id:"language",title:t.t("settings.language"),keywords:"voice language idioma voz"},{id:"confidence",title:t.t("settings.confidence"),keywords:"voice confidence confianza"},{id:"test",title:t.t("settings.test"),keywords:"voice microphone microphone test microfono"},{id:"voice",title:t.t("speech.voice"),keywords:"read aloud voice voz lectura altavoz"},{id:"tts-engine",title:t.t("speech.engine"),keywords:"voice engine motor edge neural sistema"},{id:"whisper-python",title:t.t("whisper.python"),keywords:"whisper python venv entorno"},{id:"whisper-model",title:t.t("whisper.model"),keywords:"whisper model modelo"},{id:"whisper-language",title:t.t("whisper.language"),keywords:"whisper language idioma"},{id:"whisper-test",title:t.t("whisper.test"),keywords:"whisper check comprobar"},{id:"whispercpp-model",title:t.t("whispercpp.model"),keywords:"whisper.cpp model modelo"},{id:"whispercpp-language",title:t.t("whispercpp.language"),keywords:"whisper.cpp language idioma"},{id:"whispercpp-ffmpeg",title:t.t("whispercpp.ffmpeg"),keywords:"whisper.cpp ffmpeg audio convertir"},{id:"whispercpp-install",title:t.t("whispercpp.install"),keywords:"whisper.cpp download descargar instalar"},{id:"whispercpp-test",title:t.t("whispercpp.test"),keywords:"whisper.cpp check comprobar"},{id:"rate",title:t.t("speech.rate"),keywords:"read aloud speed velocidad lectura"},{id:"skip-code",title:t.t("speech.skipCode"),keywords:"read aloud code codigo lectura"},{id:"preview",title:t.t("speech.preview"),keywords:"read aloud test voice probar voz"}]},render:()=>o.createComponent(qe,{speaker:n})})},Ke=$.Plugin.define({...$e,setup:Xe,main:Fe});exports.default=Ke;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-voice-dictation",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Offline voice dictation and read-aloud for OpenCode Desktop on Windows and Linux",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "Offline voice dictation and read-aloud for OpenCode Desktop and TUI on Windows and Linux",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Fran Carlos <francarlos803@gmail.com>",
|
|
7
7
|
"homepage": "https://github.com/evious/opencode-voice-dictation#readme",
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
"keywords": [
|
|
16
16
|
"opencode",
|
|
17
17
|
"opencode-plugin",
|
|
18
|
+
"tui",
|
|
19
|
+
"terminal",
|
|
18
20
|
"voice",
|
|
19
21
|
"dictation",
|
|
20
22
|
"speech-to-text",
|
|
@@ -28,7 +30,8 @@
|
|
|
28
30
|
"exports": {
|
|
29
31
|
".": "./src/index.ts",
|
|
30
32
|
"./gui-experimental": "./dist/gui-experimental.cjs",
|
|
31
|
-
"./rpc": "./src/rpc.ts"
|
|
33
|
+
"./rpc": "./src/rpc.ts",
|
|
34
|
+
"./tui": "./src/tui.ts"
|
|
32
35
|
},
|
|
33
36
|
"files": [
|
|
34
37
|
"src",
|
|
@@ -38,8 +41,8 @@
|
|
|
38
41
|
"docs"
|
|
39
42
|
],
|
|
40
43
|
"dependencies": {
|
|
41
|
-
"
|
|
42
|
-
"
|
|
44
|
+
"@opencode/plugin": "2.2.6",
|
|
45
|
+
"msedge-tts": "^2.1.1"
|
|
43
46
|
},
|
|
44
47
|
"publishConfig": {
|
|
45
48
|
"access": "public"
|
|
@@ -47,11 +50,14 @@
|
|
|
47
50
|
"scripts": {
|
|
48
51
|
"build": "vite build",
|
|
49
52
|
"dev": "vite build --watch",
|
|
50
|
-
"test": "node test/smoke.cjs",
|
|
53
|
+
"test": "node test/speech-text.cjs && node test/tui.cjs && node test/smoke.cjs",
|
|
51
54
|
"typecheck": "tsc --noEmit",
|
|
52
55
|
"prepublishOnly": "npm run typecheck && npm run build && npm test"
|
|
53
56
|
},
|
|
54
57
|
"devDependencies": {
|
|
58
|
+
"@opencode/theme": "^2.2.6",
|
|
59
|
+
"@opentui/core": "^0.8.1",
|
|
60
|
+
"@opentui/solid": "^0.8.1",
|
|
55
61
|
"@types/node": "^24.0.0",
|
|
56
62
|
"effect": "4.1.0",
|
|
57
63
|
"solid-js": "^1.9.17",
|
package/src/gui/main.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode/plugin/gui"
|
|
2
2
|
import { Dictation, Speech, VoiceTyping } from "./contract"
|
|
3
3
|
import { diagnose, startEngine, type EngineHandle } from "./engines/system-speech"
|
|
4
|
-
import { listSapiVoices, speakSapi, type SpeakHandle } from "
|
|
5
|
-
import { sendVoiceTypingShortcut } from "
|
|
4
|
+
import { listSapiVoices, speakSapi, type SpeakHandle } from "../sapi-speech"
|
|
5
|
+
import { sendVoiceTypingShortcut } from "../win-h"
|
|
6
6
|
|
|
7
7
|
type Status = "idle" | "starting" | "listening" | "error"
|
|
8
8
|
|
package/src/gui/settings.tsx
CHANGED
|
@@ -274,7 +274,7 @@ export function VoiceSettings(props: { speaker: Speaker }) {
|
|
|
274
274
|
title: ctx.t("pill.title"),
|
|
275
275
|
description: ctx.t("whispercpp.ffmpeg.install.ok"),
|
|
276
276
|
})
|
|
277
|
-
await readWhisperCppStatus()
|
|
277
|
+
await readWhisperCppStatus().catch(() => {})
|
|
278
278
|
} else {
|
|
279
279
|
const manual = result.command
|
|
280
280
|
? ` ${ctx.t("whispercpp.ffmpeg.install.manual", { command: result.command })}`
|
package/src/gui/speak.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createSignal } from "solid-js"
|
|
2
2
|
import type { IpcClient, SetupContext } from "@opencode/plugin/gui"
|
|
3
3
|
import { VoiceRpc } from "../rpc"
|
|
4
|
+
import { cleanForSpeech, chunkForSpeech } from "../speech-text"
|
|
4
5
|
import { Speech } from "./contract"
|
|
5
6
|
import type { definition } from "./definition"
|
|
6
7
|
|
|
@@ -30,81 +31,7 @@ export interface Speaker {
|
|
|
30
31
|
preview(): Promise<boolean>
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
/** Turns Markdown into something worth hearing. */
|
|
36
|
-
export function cleanForSpeech(text: string, skipCode: boolean): string {
|
|
37
|
-
let value = text.replace(/\r\n/g, "\n")
|
|
38
|
-
|
|
39
|
-
if (skipCode) {
|
|
40
|
-
value = value.replace(/```[\s\S]*?```/g, " ")
|
|
41
|
-
} else {
|
|
42
|
-
value = value.replace(/```[^\n]*\n([\s\S]*?)```/g, "$1")
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
value = value.replace(/`([^`]+)`/g, "$1")
|
|
46
|
-
value = value.replace(/!\[[^\]]*\]\([^)]*\)/g, " ")
|
|
47
|
-
value = value.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
|
|
48
|
-
value = value.replace(/^\s*#{1,6}\s+/gm, "")
|
|
49
|
-
value = value.replace(/^\s*>\s?/gm, "")
|
|
50
|
-
value = value.replace(/^\s*[-*+]\s+/gm, "")
|
|
51
|
-
value = value.replace(/^\s*\d+\.\s+/gm, "")
|
|
52
|
-
value = value.replace(/(\*\*|__)(.*?)\1/g, "$2")
|
|
53
|
-
value = value.replace(/(\*|_)(.*?)\1/g, "$2")
|
|
54
|
-
value = value.replace(/^\s*\|.*\|\s*$/gm, " ")
|
|
55
|
-
value = value.replace(/\n{2,}/g, ". ")
|
|
56
|
-
value = value.replace(/\n/g, " ")
|
|
57
|
-
value = value.replace(/\s+/g, " ")
|
|
58
|
-
value = value.replace(/\s+([.,;:!?])/g, "$1")
|
|
59
|
-
value = value.replace(/([.,;:!?]){2,}/g, "$1")
|
|
60
|
-
|
|
61
|
-
return value.trim()
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Splits at sentence boundaries, then at word boundaries for long sentences. */
|
|
65
|
-
export function chunkForSpeech(text: string, max = MAX_CHUNK): string[] {
|
|
66
|
-
const trimmed = text.trim()
|
|
67
|
-
if (!trimmed) return []
|
|
68
|
-
|
|
69
|
-
const sentences = trimmed.split(/(?<=[.!?…])\s+/)
|
|
70
|
-
const chunks: string[] = []
|
|
71
|
-
let current = ""
|
|
72
|
-
|
|
73
|
-
const pushWords = (sentence: string) => {
|
|
74
|
-
let part = ""
|
|
75
|
-
for (const word of sentence.split(/\s+/)) {
|
|
76
|
-
if (!word) continue
|
|
77
|
-
if (part && part.length + 1 + word.length > max) {
|
|
78
|
-
chunks.push(part)
|
|
79
|
-
part = word
|
|
80
|
-
} else {
|
|
81
|
-
part = part ? `${part} ${word}` : word
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return part
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
for (const sentence of sentences) {
|
|
88
|
-
if (sentence.length > max) {
|
|
89
|
-
if (current) {
|
|
90
|
-
chunks.push(current)
|
|
91
|
-
current = ""
|
|
92
|
-
}
|
|
93
|
-
current = pushWords(sentence)
|
|
94
|
-
continue
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (current && current.length + 1 + sentence.length > max) {
|
|
98
|
-
chunks.push(current)
|
|
99
|
-
current = sentence
|
|
100
|
-
} else {
|
|
101
|
-
current = current ? `${current} ${sentence}` : sentence
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (current) chunks.push(current)
|
|
106
|
-
return chunks
|
|
107
|
-
}
|
|
34
|
+
export { cleanForSpeech, chunkForSpeech } from "../speech-text"
|
|
108
35
|
|
|
109
36
|
export function createSpeaker(
|
|
110
37
|
ctx: SetupContext<typeof definition>,
|
|
@@ -3,7 +3,7 @@ import { createHash, randomUUID } from "node:crypto"
|
|
|
3
3
|
import { mkdirSync, rmSync, writeFileSync } from "node:fs"
|
|
4
4
|
import { tmpdir } from "node:os"
|
|
5
5
|
import { join } from "node:path"
|
|
6
|
-
import { SPEAK_SCRIPT } from "./speak-script"
|
|
6
|
+
import { SPEAK_SCRIPT } from "./speak-script.js"
|
|
7
7
|
|
|
8
8
|
const POWERSHELL = "powershell.exe"
|
|
9
9
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const MAX_CHUNK = 250
|
|
2
|
+
|
|
3
|
+
/** Turns Markdown into something worth hearing. */
|
|
4
|
+
export function cleanForSpeech(text: string, skipCode: boolean): string {
|
|
5
|
+
let value = text.replace(/\r\n/g, "\n")
|
|
6
|
+
|
|
7
|
+
if (skipCode) {
|
|
8
|
+
value = value.replace(/```[\s\S]*?```/g, " ")
|
|
9
|
+
} else {
|
|
10
|
+
value = value.replace(/```[^\n]*\n([\s\S]*?)```/g, "$1")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
value = value.replace(/`([^`]+)`/g, "$1")
|
|
14
|
+
value = value.replace(/!\[[^\]]*\]\([^)]*\)/g, " ")
|
|
15
|
+
value = value.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
|
|
16
|
+
value = value.replace(/^\s*#{1,6}\s+/gm, "")
|
|
17
|
+
value = value.replace(/^\s*>\s?/gm, "")
|
|
18
|
+
value = value.replace(/^\s*[-*+]\s+/gm, "")
|
|
19
|
+
value = value.replace(/^\s*\d+\.\s+/gm, "")
|
|
20
|
+
value = value.replace(/(\*\*|__)(.*?)\1/g, "$2")
|
|
21
|
+
value = value.replace(/(\*|_)(.*?)\1/g, "$2")
|
|
22
|
+
value = value.replace(/^\s*\|.*\|\s*$/gm, " ")
|
|
23
|
+
value = value.replace(/\n{2,}/g, ". ")
|
|
24
|
+
value = value.replace(/\n/g, " ")
|
|
25
|
+
value = value.replace(/\s+/g, " ")
|
|
26
|
+
value = value.replace(/\s+([.,;:!?])/g, "$1")
|
|
27
|
+
value = value.replace(/([.,;:!?]){2,}/g, "$1")
|
|
28
|
+
|
|
29
|
+
return value.trim()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Splits at sentence boundaries, then at word boundaries for long sentences. */
|
|
33
|
+
export function chunkForSpeech(text: string, max = MAX_CHUNK): string[] {
|
|
34
|
+
const trimmed = text.trim()
|
|
35
|
+
if (!trimmed) return []
|
|
36
|
+
|
|
37
|
+
const sentences = trimmed.split(/(?<=[.!?…])\s+/)
|
|
38
|
+
const chunks: string[] = []
|
|
39
|
+
let current = ""
|
|
40
|
+
|
|
41
|
+
const pushWords = (sentence: string) => {
|
|
42
|
+
let part = ""
|
|
43
|
+
for (const word of sentence.split(/\s+/)) {
|
|
44
|
+
if (!word) continue
|
|
45
|
+
if (part && part.length + 1 + word.length > max) {
|
|
46
|
+
chunks.push(part)
|
|
47
|
+
part = word
|
|
48
|
+
} else {
|
|
49
|
+
part = part ? `${part} ${word}` : word
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return part
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const sentence of sentences) {
|
|
56
|
+
if (sentence.length > max) {
|
|
57
|
+
if (current) {
|
|
58
|
+
chunks.push(current)
|
|
59
|
+
current = ""
|
|
60
|
+
}
|
|
61
|
+
current = pushWords(sentence)
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (current && current.length + 1 + sentence.length > max) {
|
|
66
|
+
chunks.push(current)
|
|
67
|
+
current = sentence
|
|
68
|
+
} else {
|
|
69
|
+
current = current ? `${current} ${sentence}` : sentence
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (current) chunks.push(current)
|
|
74
|
+
return chunks
|
|
75
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process"
|
|
2
|
+
import { randomUUID } from "node:crypto"
|
|
3
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs"
|
|
4
|
+
import { tmpdir } from "node:os"
|
|
5
|
+
import { join } from "node:path"
|
|
6
|
+
|
|
7
|
+
const POWERSHELL = "powershell.exe"
|
|
8
|
+
|
|
9
|
+
const PLAYER_SCRIPT = `param(
|
|
10
|
+
[Parameter(Mandatory = $true)][string]$Path
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
$ErrorActionPreference = "Stop"
|
|
14
|
+
$player = $null
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
[Windows.Media.Playback.MediaPlayer, Windows.Media.Playback, ContentType = WindowsRuntime] | Out-Null
|
|
18
|
+
[Windows.Media.Core.MediaSource, Windows.Media.Core, ContentType = WindowsRuntime] | Out-Null
|
|
19
|
+
|
|
20
|
+
$player = [Windows.Media.Playback.MediaPlayer]::new()
|
|
21
|
+
$player.Source = [Windows.Media.Core.MediaSource]::CreateFromUri([Uri]$Path)
|
|
22
|
+
$player.Play()
|
|
23
|
+
|
|
24
|
+
$deadline = [DateTime]::UtcNow.AddSeconds(5)
|
|
25
|
+
$duration = 0
|
|
26
|
+
do {
|
|
27
|
+
Start-Sleep -Milliseconds 50
|
|
28
|
+
$duration = $player.PlaybackSession.NaturalDuration.TotalMilliseconds
|
|
29
|
+
} while ($duration -le 0 -and [DateTime]::UtcNow -lt $deadline)
|
|
30
|
+
|
|
31
|
+
if ($duration -le 0) {
|
|
32
|
+
$player.Dispose()
|
|
33
|
+
exit 5
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
Start-Sleep -Milliseconds ([int]$duration + 350)
|
|
37
|
+
$player.Pause()
|
|
38
|
+
$player.Dispose()
|
|
39
|
+
exit 0
|
|
40
|
+
} catch {
|
|
41
|
+
if ($null -ne $player) {
|
|
42
|
+
try { $player.Dispose() } catch { }
|
|
43
|
+
}
|
|
44
|
+
Write-Error $_
|
|
45
|
+
exit 4
|
|
46
|
+
}
|
|
47
|
+
`
|
|
48
|
+
|
|
49
|
+
export interface Playback {
|
|
50
|
+
stop(): void
|
|
51
|
+
done: Promise<boolean>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function workDir(): string {
|
|
55
|
+
const dir = join(tmpdir(), "opencode-voice-dictation")
|
|
56
|
+
mkdirSync(dir, { recursive: true })
|
|
57
|
+
return dir
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function killTree(child: ChildProcess) {
|
|
61
|
+
const pid = child.pid
|
|
62
|
+
if (pid === undefined) {
|
|
63
|
+
child.kill()
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
const killer = spawn("taskkill", ["/PID", String(pid), "/T", "/F"], {
|
|
67
|
+
windowsHide: true,
|
|
68
|
+
stdio: "ignore",
|
|
69
|
+
})
|
|
70
|
+
killer.on("error", () => child.kill())
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function playMp3Base64(base64: string): Playback {
|
|
74
|
+
if (process.platform !== "win32") {
|
|
75
|
+
return { stop() {}, done: Promise.resolve(false) }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const id = randomUUID()
|
|
79
|
+
const file = join(workDir(), `speech-${id}.mp3`)
|
|
80
|
+
const script = join(workDir(), `speech-${id}.ps1`)
|
|
81
|
+
writeFileSync(file, Buffer.from(base64, "base64"))
|
|
82
|
+
writeFileSync(script, PLAYER_SCRIPT, "utf8")
|
|
83
|
+
|
|
84
|
+
let child: ChildProcess | undefined
|
|
85
|
+
let settled = false
|
|
86
|
+
let resolveDone!: (ok: boolean) => void
|
|
87
|
+
const done = new Promise<boolean>((resolve) => {
|
|
88
|
+
resolveDone = resolve
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const clean = () => {
|
|
92
|
+
for (const path of [file, script]) {
|
|
93
|
+
try {
|
|
94
|
+
rmSync(path, { force: true })
|
|
95
|
+
} catch {
|
|
96
|
+
// Best effort: the temp folder is cleaned by the OS anyway.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const finish = (ok: boolean) => {
|
|
102
|
+
if (settled) return
|
|
103
|
+
settled = true
|
|
104
|
+
clean()
|
|
105
|
+
resolveDone(ok)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
child = spawn(
|
|
109
|
+
POWERSHELL,
|
|
110
|
+
["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", script, "-Path", file],
|
|
111
|
+
{
|
|
112
|
+
windowsHide: true,
|
|
113
|
+
stdio: "ignore",
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
child.on("error", () => finish(false))
|
|
118
|
+
child.on("close", (code) => finish(code === 0))
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
stop() {
|
|
122
|
+
if (settled) return
|
|
123
|
+
if (child) killTree(child)
|
|
124
|
+
finish(false)
|
|
125
|
+
},
|
|
126
|
+
done,
|
|
127
|
+
}
|
|
128
|
+
}
|
package/src/tui.ts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { Plugin } from "@opencode/plugin/tui"
|
|
2
|
+
import { VoiceRpc } from "./rpc.js"
|
|
3
|
+
import { chunkForSpeech, cleanForSpeech } from "./speech-text.js"
|
|
4
|
+
import { speakSapi, type SpeakHandle } from "./sapi-speech.js"
|
|
5
|
+
import { playMp3Base64, type Playback } from "./tui/player.js"
|
|
6
|
+
import { sendVoiceTypingShortcut } from "./win-h.js"
|
|
7
|
+
|
|
8
|
+
const DEFAULT_VOICE = "es-ES-XimenaNeural"
|
|
9
|
+
const DEFAULT_RATE = 1
|
|
10
|
+
const MAX_EDGE_CHUNK = 4000
|
|
11
|
+
|
|
12
|
+
function errorMessage(error: unknown): string {
|
|
13
|
+
if (error instanceof Error && error.message) return error.message
|
|
14
|
+
if (typeof error === "string" && error) return error
|
|
15
|
+
return "error desconocido"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default Plugin.define({
|
|
19
|
+
id: "voice-dictation.tui",
|
|
20
|
+
setup(context) {
|
|
21
|
+
const voice = typeof context.options.voice === "string" ? context.options.voice : DEFAULT_VOICE
|
|
22
|
+
const rate = typeof context.options.rate === "number" ? context.options.rate : DEFAULT_RATE
|
|
23
|
+
const skipCode = typeof context.options.skipCode === "boolean" ? context.options.skipCode : true
|
|
24
|
+
|
|
25
|
+
let generation = 0
|
|
26
|
+
let busy = false
|
|
27
|
+
let active: { stop(): void } | undefined
|
|
28
|
+
|
|
29
|
+
const stop = () => {
|
|
30
|
+
generation++
|
|
31
|
+
active?.stop()
|
|
32
|
+
active = undefined
|
|
33
|
+
busy = false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const dictate = async () => {
|
|
37
|
+
if (process.platform !== "win32") {
|
|
38
|
+
context.ui.toast.show({
|
|
39
|
+
title: "Dictado por voz",
|
|
40
|
+
message: "El dictado de la TUI está disponible solo en Windows por ahora.",
|
|
41
|
+
variant: "warning",
|
|
42
|
+
})
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
await sendVoiceTypingShortcut()
|
|
48
|
+
context.ui.toast.show({
|
|
49
|
+
title: "Dictado por voz",
|
|
50
|
+
message: "Dictado de Windows abierto. Habla y el texto aparecerá en el prompt.",
|
|
51
|
+
variant: "info",
|
|
52
|
+
})
|
|
53
|
+
} catch (error) {
|
|
54
|
+
context.ui.toast.show({
|
|
55
|
+
title: "Dictado por voz",
|
|
56
|
+
message: `No se pudo abrir el dictado: ${errorMessage(error)}`,
|
|
57
|
+
variant: "error",
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const speakSystem = (text: string, current: number) => {
|
|
63
|
+
return new Promise<void>((resolve) => {
|
|
64
|
+
let settled = false
|
|
65
|
+
let wrapper: { stop(): void } | undefined
|
|
66
|
+
|
|
67
|
+
const finish = () => {
|
|
68
|
+
if (settled) return
|
|
69
|
+
settled = true
|
|
70
|
+
if (active === wrapper) active = undefined
|
|
71
|
+
resolve()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const handle: SpeakHandle = speakSapi({ text, rate, voice: "" }, (ok) => {
|
|
75
|
+
if (!ok && current === generation) {
|
|
76
|
+
context.ui.toast.show({
|
|
77
|
+
title: "Dictado por voz",
|
|
78
|
+
message: "Tampoco se pudo reproducir con la voz del sistema.",
|
|
79
|
+
variant: "error",
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
finish()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
wrapper = {
|
|
86
|
+
stop() {
|
|
87
|
+
handle.stop()
|
|
88
|
+
finish()
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
active = wrapper
|
|
92
|
+
|
|
93
|
+
if (current !== generation) {
|
|
94
|
+
handle.stop()
|
|
95
|
+
finish()
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const read = async () => {
|
|
101
|
+
if (busy || active) {
|
|
102
|
+
stop()
|
|
103
|
+
context.ui.toast.show({
|
|
104
|
+
title: "Dictado por voz",
|
|
105
|
+
message: "Lectura detenida.",
|
|
106
|
+
variant: "info",
|
|
107
|
+
})
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (process.platform !== "win32") {
|
|
112
|
+
context.ui.toast.show({
|
|
113
|
+
title: "Dictado por voz",
|
|
114
|
+
message: "La lectura de la TUI está disponible solo en Windows por ahora.",
|
|
115
|
+
variant: "warning",
|
|
116
|
+
})
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const route = context.ui.router.current()
|
|
121
|
+
if (route.type !== "session") {
|
|
122
|
+
context.ui.toast.show({
|
|
123
|
+
title: "Dictado por voz",
|
|
124
|
+
message: "Abre una sesión para leer su última respuesta.",
|
|
125
|
+
variant: "warning",
|
|
126
|
+
})
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const current = ++generation
|
|
131
|
+
busy = true
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const response = await context.client.message.list({
|
|
135
|
+
sessionID: route.sessionID,
|
|
136
|
+
type: "assistant",
|
|
137
|
+
order: "desc",
|
|
138
|
+
limit: 10,
|
|
139
|
+
})
|
|
140
|
+
if (current !== generation) return
|
|
141
|
+
|
|
142
|
+
const message = response.data.find(
|
|
143
|
+
(entry) =>
|
|
144
|
+
entry.type === "assistant" &&
|
|
145
|
+
entry.content.some((part) => part.type === "text" && part.text.trim().length > 0),
|
|
146
|
+
)
|
|
147
|
+
if (!message || message.type !== "assistant") {
|
|
148
|
+
context.ui.toast.show({
|
|
149
|
+
title: "Dictado por voz",
|
|
150
|
+
message: "No hay una respuesta para leer.",
|
|
151
|
+
variant: "info",
|
|
152
|
+
})
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const text = message.content
|
|
157
|
+
.filter((part) => part.type === "text")
|
|
158
|
+
.map((part) => part.text)
|
|
159
|
+
.join("\n")
|
|
160
|
+
const cleaned = cleanForSpeech(text, skipCode)
|
|
161
|
+
if (!cleaned) {
|
|
162
|
+
context.ui.toast.show({
|
|
163
|
+
title: "Dictado por voz",
|
|
164
|
+
message: "La última respuesta no tiene texto para leer.",
|
|
165
|
+
variant: "info",
|
|
166
|
+
})
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let edgeFailed = false
|
|
171
|
+
for (const chunk of chunkForSpeech(cleaned, MAX_EDGE_CHUNK)) {
|
|
172
|
+
if (current !== generation) return
|
|
173
|
+
|
|
174
|
+
let audio: string
|
|
175
|
+
try {
|
|
176
|
+
const result = await context.client.rpc(VoiceRpc).edgeSpeak({ text: chunk, voice, rate })
|
|
177
|
+
if (typeof result !== "string" || result.length === 0) {
|
|
178
|
+
edgeFailed = true
|
|
179
|
+
break
|
|
180
|
+
}
|
|
181
|
+
audio = result
|
|
182
|
+
} catch {
|
|
183
|
+
edgeFailed = true
|
|
184
|
+
break
|
|
185
|
+
}
|
|
186
|
+
if (current !== generation) return
|
|
187
|
+
|
|
188
|
+
const playback: Playback = playMp3Base64(audio)
|
|
189
|
+
active = playback
|
|
190
|
+
const ok = await playback.done
|
|
191
|
+
if (active === playback) active = undefined
|
|
192
|
+
if (current !== generation) return
|
|
193
|
+
|
|
194
|
+
if (!ok) {
|
|
195
|
+
edgeFailed = true
|
|
196
|
+
break
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!edgeFailed || current !== generation) return
|
|
201
|
+
|
|
202
|
+
context.ui.toast.show({
|
|
203
|
+
title: "Dictado por voz",
|
|
204
|
+
message: "No se pudo usar Edge. Usando la voz del sistema.",
|
|
205
|
+
variant: "warning",
|
|
206
|
+
})
|
|
207
|
+
await speakSystem(cleaned, current)
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (current === generation) {
|
|
210
|
+
context.ui.toast.show({
|
|
211
|
+
title: "Dictado por voz",
|
|
212
|
+
message: `No se pudo leer la respuesta: ${errorMessage(error)}`,
|
|
213
|
+
variant: "error",
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
} finally {
|
|
217
|
+
if (current === generation) busy = false
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
context.keymap.layer(() => ({
|
|
222
|
+
mode: "global",
|
|
223
|
+
priority: 10,
|
|
224
|
+
commands: [
|
|
225
|
+
{
|
|
226
|
+
id: "voice-dictation.tui.dictate",
|
|
227
|
+
title: "Dictar por voz",
|
|
228
|
+
description: "Abre el dictado de Windows (Win+H) en el prompt.",
|
|
229
|
+
group: "Voz",
|
|
230
|
+
bind: "<leader>d",
|
|
231
|
+
palette: true,
|
|
232
|
+
slash: { name: "dictado", aliases: ["dictate"] },
|
|
233
|
+
suggested: true,
|
|
234
|
+
run: dictate,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: "voice-dictation.tui.read",
|
|
238
|
+
title: "Leer última respuesta",
|
|
239
|
+
description: "Lee en voz alta la última respuesta con voces Edge.",
|
|
240
|
+
group: "Voz",
|
|
241
|
+
bind: "<leader>v",
|
|
242
|
+
palette: true,
|
|
243
|
+
slash: { name: "leer", aliases: ["read"] },
|
|
244
|
+
suggested: true,
|
|
245
|
+
run: read,
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
bindings: ["voice-dictation.tui.dictate", "voice-dictation.tui.read"],
|
|
249
|
+
}))
|
|
250
|
+
|
|
251
|
+
return () => stop()
|
|
252
|
+
},
|
|
253
|
+
})
|
|
File without changes
|
|
File without changes
|