opencode-voice-plugin 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,389 @@
1
+ # opencode-voice-plugin
2
+
3
+ [English](README.md) · **Português (BR)**
4
+
5
+ Ditado por voz para o **OpenCode V2**: um ícone de microfone (`○`) no rodapé do prompt. Clique nele
6
+ (ou use `ctrl+y`), fale, e o texto transcrito vira uma mensagem da sessão.
7
+
8
+ A transcrição usa o **OpenRouter** (`POST /api/v1/audio/transcriptions`) e a chave é reaproveitada do
9
+ próprio OpenCode quando você já tem o OpenRouter conectado — sem variável de ambiente e sem duplicar
10
+ segredo. Funciona em **Windows, Linux e macOS** a partir do mesmo código, e é instalado **na máquina**
11
+ (não por projeto).
12
+
13
+ [Repositório](https://github.com/kabrau/opencode-voice-plugin) ·
14
+ [Issues](https://github.com/kabrau/opencode-voice-plugin/issues) ·
15
+ [Design](DESIGN.md) · MIT
16
+
17
+ ---
18
+
19
+ ## Como funciona
20
+
21
+ 1. `○` no rodapé do prompt (aparece dentro de uma sessão).
22
+ 2. Clique, ou `ctrl+y`, ou digite `/voice` → começa a gravar (`●` + segundos).
23
+ 3. Clique de novo (ou espere `maxSeconds`) → para, transcreve (`…`).
24
+ 4. Abre o dialog **Ditado** com o texto — edite se quiser e confirme → a mensagem é enviada na sessão.
25
+
26
+ Na **tela inicial** também funciona: como ali ainda não existe sessão, o `ctrl+y` **cria a sessão na
27
+ hora** (antes de gravar) e o texto é enviado nela — o mesmo que digitar e dar Enter. Desligue com
28
+ `"allowFromHome": false` se preferir que o microfone só aja dentro de uma sessão.
29
+
30
+ ---
31
+
32
+ ## Requisitos
33
+
34
+ - OpenCode **V2** (`opencode --version`).
35
+ - **ffmpeg** no sistema:
36
+ - Windows: `choco install ffmpeg`
37
+ - Debian/Ubuntu: `sudo apt install ffmpeg pulseaudio-utils`
38
+ - macOS: `brew install ffmpeg`
39
+ - Uma chave de API do **OpenRouter** (veja [Custo](#custo) e [Chave da API](#chave-da-api)).
40
+
41
+ ---
42
+
43
+ ## Instalação (via npm — recomendado)
44
+
45
+ ```bash
46
+ opencode plugin add opencode-voice-plugin
47
+ ```
48
+
49
+ Pronto — sem clonar nem copiar arquivo. Atualizar e remover:
50
+
51
+ ```bash
52
+ opencode plugin update opencode-voice-plugin
53
+ opencode plugin remove opencode-voice-plugin
54
+ ```
55
+
56
+ > Para **configurar options** (ícones, atalho, dispositivo, etc.), veja
57
+ > [Onde declarar as options](#onde-declarar-as-options) — é um detalhe que o `plugin add`
58
+ > sozinho não cobre.
59
+
60
+ ### Sem npm (direto do GitHub)
61
+
62
+ ```bash
63
+ opencode plugin add github:kabrau/opencode-voice-plugin
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Instalação manual (na máquina)
69
+
70
+ Se preferir cópia local, o OpenCode descobre plugins no diretório global de configuração:
71
+
72
+ ```
73
+ <config-global>/plugins/<nome>/index.ts # entrypoint de servidor
74
+ <config-global>/plugins/<nome>/tui.tsx # entrypoint da TUI (o plugin)
75
+ ```
76
+
77
+ No Windows o config global fica em `C:\Users\<você>\.config\opencode`. No Linux/macOS, em
78
+ `~/.config/opencode` (ou `$XDG_CONFIG_HOME/opencode`).
79
+
80
+ ### Windows (PowerShell)
81
+
82
+ ```powershell
83
+ git clone https://github.com/kabrau/opencode-voice-plugin.git
84
+ $destino = "$env:USERPROFILE\.config\opencode\plugins\voice"
85
+ New-Item -ItemType Directory -Force -Path $destino | Out-Null
86
+ Copy-Item .\opencode-voice-plugin\src\* $destino
87
+ ```
88
+
89
+ ### Linux / macOS
90
+
91
+ ```bash
92
+ git clone https://github.com/kabrau/opencode-voice-plugin.git
93
+ destino="${XDG_CONFIG_HOME:-$HOME/.config}/opencode/plugins/voice"
94
+ mkdir -p "$destino"
95
+ cp opencode-voice-plugin/src/* "$destino/"
96
+ ```
97
+
98
+ Depois, **reinicie o `opencode`**.
99
+
100
+ ### Instalação alternativa (apontando para o clone)
101
+
102
+ Aponte o `cli.json` global para o repositório clonado. Isso mantém o plugin atualizado via `git pull`
103
+ e é o jeito de passar options, já que a pasta de descoberta não tem onde declará-las:
104
+
105
+ ```json
106
+ {
107
+ "plugins": [
108
+ {
109
+ "package": "file:///caminho/para/opencode-voice-plugin",
110
+ "options": {
111
+ "language": "pt",
112
+ "keybind": "ctrl+y"
113
+ }
114
+ }
115
+ ]
116
+ }
117
+ ```
118
+
119
+ > O caminho pode ser um caminho absoluto ou uma URL `file://`. **Adicione** ao array `plugins`
120
+ > existente em `~/.config/opencode/cli.json`, sem apagar o resto do arquivo.
121
+
122
+ ---
123
+
124
+ ## Chave da API
125
+
126
+ O plugin resolve a chave nesta ordem:
127
+
128
+ 1. **Variável de ambiente** — `OPENROUTER_API_KEY` (ou o nome em `apiKeyEnv`), se existir.
129
+ 2. **Store do próprio OpenCode** — se você já conectou o OpenRouter, o plugin usa essa chave
130
+ automaticamente. **Nada a configurar.**
131
+
132
+ Ou seja: se o OpenRouter já aparece conectado, você **não precisa** criar variável nenhuma.
133
+ Confira com:
134
+
135
+ ```bash
136
+ opencode auth list # deve listar "OpenRouter ... stored"
137
+ ```
138
+
139
+ Se preferir a env var (ou não tiver o OpenRouter conectado):
140
+
141
+ ```powershell
142
+ setx OPENROUTER_API_KEY "sk-or-..." # Windows; reabra o terminal depois
143
+ ```
144
+
145
+ ```bash
146
+ export OPENROUTER_API_KEY="sk-or-..." # Linux/macOS (~/.bashrc / ~/.zshrc)
147
+ ```
148
+
149
+ Gere uma chave em <https://openrouter.ai/settings/keys>. Nos dois casos a chave **nunca** é
150
+ logada, copiada nem gravada pelo plugin — e a leitura do store é somente-leitura, do arquivo
151
+ que o próprio OpenCode mantém (`<data>/opencode/auth.json`).
152
+
153
+ Para uma postura "só env var", use `"apiKeyFromAuth": false` nas options; para ler de outra
154
+ integração (ex.: a do Console), ajuste `authIntegration`.
155
+
156
+ ---
157
+
158
+ ## Configuração
159
+
160
+ ### Onde declarar as options
161
+
162
+ As options são lidas do **`cli.json`** (config global), **não** do `opencode.jsonc`. Isso vale
163
+ mesmo que você tenha instalado por cópia em `plugins/voice/`: adicione a entrada abaixo ao array
164
+ `plugins` do `~/.config/opencode/cli.json`, preservando o resto do arquivo.
165
+
166
+ ```json
167
+ {
168
+ "plugins": [
169
+ {
170
+ "package": "C:\\Users\\<você>\\.config\\opencode\\plugins\\voice",
171
+ "options": {
172
+ "autoSubmit": true
173
+ }
174
+ }
175
+ ]
176
+ }
177
+ ```
178
+
179
+ > Verificado nas duas direções: com essa entrada, `context.options` recebe o objeto; declarando as
180
+ > mesmas options no array `plugins` do **`opencode.jsonc`**, o componente de TUI recebe `{}`.
181
+ > Não há duplicação do plugin por estar também descoberto na pasta global.
182
+
183
+ Todas as options são opcionais.
184
+
185
+ | Option | Default | Descrição |
186
+ |---|---|---|
187
+ | `apiKeyEnv` | `"OPENROUTER_API_KEY"` | Nome da variável de ambiente com a chave. Tem prioridade sobre o store. |
188
+ | `apiKeyFromAuth` | `true` | Se a env var não existir, usa a chave já guardada pelo OpenCode. |
189
+ | `authIntegration` | `"openrouter"` | Integração do OpenCode de onde ler a chave. |
190
+ | `model` | `"openai/whisper-large-v3"` | Modelo de STT do OpenRouter. |
191
+ | `language` | `"auto"` | Código ISO-639-1, ou `"auto"` para detecção automática. |
192
+ | `baseUrl` | `"https://openrouter.ai/api/v1"` | Base do provedor compatível com `/audio/transcriptions`. |
193
+ | `device` | `null` | Microfone. `null` = autodetectar. Formatos por SO abaixo. |
194
+ | `ffmpegPath` | `null` | Caminho do ffmpeg. `null` = procurar no PATH e em locais conhecidos. |
195
+ | `keybind` | `"ctrl+y"` | Atalho do ditado. Use `"<leader>v"` se preferir (`<leader>` = `ctrl+x`). |
196
+ | `autoSubmit` | `false` | `true` envia direto, sem o dialog de revisão. |
197
+ | `allowFromHome` | `true` | Permite ditar na tela inicial, criando a sessão na hora. |
198
+ | `maxSeconds` | `55` | Corte automático da gravação. Teto de segurança em `120`. |
199
+ | `keepAudio` | `false` | `true` mantém o WAV temporário (debug). |
200
+ | `icons` | `{ idle: "○", recording: "●", transcribing: "…" }` | Glifos do ícone. Sobrescreva se sua fonte suportar emoji. |
201
+
202
+ > **Por que os glifos padrão são `○ ● …` e não 🎤:** emojis fora do BMP (como `🎤`) não
203
+ > existem na fonte de muitos consoles e aparecem como `?`; além disso a largura deles é
204
+ > ambígua, o que **desalinha a área clicável** (o clique roteia por hit-test de coordenadas).
205
+ > Os glifos padrão são do BMP, largura 1 e existem em Consolas/Cascadia/DejaVu. Se o seu
206
+ > terminal renderiza emoji corretamente, use por exemplo:
207
+ >
208
+ > ```json
209
+ > { "options": { "icons": { "idle": "🎤", "recording": "🔴", "transcribing": "⏳" } } }
210
+ > ```
211
+
212
+ Formatos de `device`:
213
+
214
+ | SO | Formato | Exemplo |
215
+ |---|---|---|
216
+ | Windows | nome exato do DirectShow | `"Microphone (C922 Pro Stream Webcam)"` |
217
+ | Linux | nome do source Pulse/PipeWire | `"default"`, `"@DEFAULT_SOURCE@"`, `"alsa_input.pci-0000_00_1f.3.analog-stereo"` |
218
+ | Linux (ALSA) | `hw:CARD,DEV` | `"hw:1,0"` |
219
+ | macOS | índice numérico | `"0"` |
220
+
221
+ ---
222
+
223
+ ## Uso
224
+
225
+ | Ação | Como |
226
+ |---|---|
227
+ | Iniciar / parar | clique no ícone, ou `ctrl+y`, ou `/voice` |
228
+ | Escolher microfone | command palette → **"Voice: choose microphone"** (a escolha fica salva) |
229
+ | Enviar sem revisar | `"autoSubmit": true` nas options |
230
+
231
+ Estados do ícone: `○` ocioso · `● Ns` gravando · `…` transcrevendo. Erros sempre viram um toast,
232
+ nunca travam a TUI.
233
+
234
+ ---
235
+
236
+ ## Custo
237
+
238
+ O `openai/whisper-large-v3` é cobrado **por token** (o áudio é tokenizado), e o preço varia por
239
+ provedor. Valores consultados na API do OpenRouter em out/2026 (por 1M de tokens de prompt):
240
+
241
+ | Provedor | Preço (prompt) |
242
+ |---|---|
243
+ | DeepInfra | US$ 7,50 / 1M tokens |
244
+ | Together | US$ 25,00 / 1M tokens |
245
+ | Groq | US$ 30,83 / 1M tokens |
246
+
247
+ O OpenRouter roteia automaticamente para um provedor disponível — o custo real de cada requisição
248
+ vem no campo `usage.cost` da resposta. Como ordem de grandeza, o exemplo da própria documentação do
249
+ OpenRouter mostra uma transcrição de 9,2 s custando **US$ 0,000508**.
250
+
251
+ **Confira os valores atuais** na página do modelo: <https://openrouter.ai/openai/whisper-large-v3>
252
+ (e o painel de gastos em <https://openrouter.ai/activity>). Não confie nos números acima a longo prazo.
253
+
254
+ Para trocar de modelo, basta mudar `model` nas options (veja os modelos com
255
+ `output_modalities=transcription` em <https://openrouter.ai/models?output_modalities=transcription>).
256
+
257
+ ---
258
+
259
+ ## Troubleshooting
260
+
261
+ **O plugin não aparece / não carregou**
262
+ Verifique o log (existe mesmo no Windows):
263
+
264
+ ```
265
+ C:\Users\<você>\.local\share\opencode\log\opencode.log # Windows
266
+ ~/.local/share/opencode/log/opencode.log # Linux/macOS
267
+ ```
268
+
269
+ Procure por `role=cli`. Confirme que os arquivos estão em `<config-global>/plugins/voice/` e
270
+ reinicie o `opencode`.
271
+
272
+ **"ffmpeg não encontrado" (toast)**
273
+ Instale o ffmpeg (comandos acima) ou aponte `ffmpegPath` explicitamente. No Windows, o plugin já
274
+ desce automaticamente do *shim* do Chocolatey
275
+ (`C:\ProgramData\chocolatey\bin\ffmpeg.exe`) para o binário real em
276
+ `C:\ProgramData\chocolatey\lib\ffmpeg\tools\ffmpeg\bin\ffmpeg.exe`, porque o shim pode não repassar
277
+ o comando de parada ao processo de gravação.
278
+
279
+ **"Chave da API ausente" (toast)**
280
+ A variável não está visível para o processo. No Windows, `setx` só vale para **novos** terminais —
281
+ feche e reabra. Confira com `echo $env:OPENROUTER_API_KEY` (PowerShell) ou `echo $OPENROUTER_API_KEY`.
282
+
283
+ **Grava silêncio / nada é transcrito**
284
+ Provavelmente o dispositivo padrão é um cabo virtual. Rode **"Voice: choose microphone"** e selecione
285
+ o microfone de verdade. A heurística já evita nomes como `CABLE Output`, `Stereo Mix`, `loopback`,
286
+ `Virtual` e `.monitor`, mas nem todo dispositivo virtual se denuncia pelo nome.
287
+
288
+ **Linux: sem microfone / erro do PulseAudio ou ALSA**
289
+ Instale `pulseaudio-utils` (fornece o `pactl`, usado para listar os microfones). O plugin usa
290
+ `-f pulse` quando encontra o `pactl`, senão cai para `-f alsa` com `default`. Para listar os sources
291
+ manualmente:
292
+
293
+ ```bash
294
+ pactl list short sources # ignore linhas terminando em .monitor
295
+ arecord -l # alternativa via ALSA
296
+ ```
297
+
298
+ **WSL2**
299
+ O áudio chega pelo WSLg como um source do Pulse (normalmente `RDPSource`). Se `pactl info` falhar,
300
+ rode `wsl --shutdown` no Windows, reabra e verifique a permissão de microfone em
301
+ *Configurações → Privacidade → Microfone*. Em último caso, use `device` explícito.
302
+
303
+ **O ícone aparece como `?`** (ou outro caractere estranho)
304
+ Sua fonte de console não tem o glifo configurado. O padrão (`○ ● …`) funciona na maioria.
305
+ Se você trocou por emoji via `icons`, volte para o padrão ou use uma fonte com emoji.
306
+
307
+ **Não consigo clicar no ícone**
308
+ 1. Confirme que o terminal envia eventos de mouse: no **mintty/Git Bash**, veja
309
+ *Options → Mouse* (deve estar habilitado). O OpenTUI liga o mouse tracking ao iniciar.
310
+ 2. Prefira os glifos padrão (`○ ● …`). Emoji têm largura ambígua e deslocam a área clicável.
311
+ 3. Clique **em cima** do glifo (há um espaço de folga de cada lado).
312
+ 4. Enquanto isso, o atalho faz exatamente o mesmo: `ctrl+y` (ou `<leader>v`: `ctrl+x`, depois `v`), ou
313
+ `ctrl+p` → **"Voice: dictate"**, ou `/voice`.
314
+
315
+ **A transcrição falha ou demora**
316
+ O provedor estoura em cerca de **60 s por requisição**. Por isso `maxSeconds` é 55 por padrão. Se você
317
+ aumentar, o dialog avisa que gravações acima de 1 minuto podem falhar.
318
+
319
+ **O começo da fala se perde (Windows)**
320
+ O DirectShow leva cerca de 1 s para abrir o dispositivo. Espere o `●` aparecer e só então comece a
321
+ falar.
322
+
323
+ ---
324
+
325
+ ## Desenvolvimento
326
+
327
+ ```bash
328
+ npm install
329
+ npm run typecheck
330
+ ```
331
+
332
+ Sem dependências de runtime — apenas módulos nativos do Node (`child_process`, `fs`, `os`, `path`,
333
+ `buffer`), que funcionam sob o Bun embutido no OpenCode.
334
+
335
+ Toda a lógica por plataforma está isolada em `src/capture.ts`. Os demais arquivos não contêm
336
+ `if (platform)`.
337
+
338
+ > Observação: `@opencode/plugin` declara `engines: node >= 26.4.0`. No Node 24 o `npm install` emite
339
+ > um aviso `EBADENGINE`, inofensivo para o typecheck.
340
+
341
+ ### Estrutura
342
+
343
+ ```
344
+ src/index.ts entrypoint de servidor (vazio, exigido pelo layout de descoberta)
345
+ src/tui.tsx plugin: slot do ícone, atalhos, máquina de estados, envio
346
+ src/capture.ts ÚNICO módulo com lógica por plataforma
347
+ src/ffmpeg.ts resolução do binário, execução, parada limpa
348
+ src/transcribe.ts OpenRouter /audio/transcriptions
349
+ src/settings.ts options + env + defaults
350
+ ```
351
+
352
+ As decisões de design e a pesquisa de API (o que foi verificado e o que **não** é possível)
353
+ estão em [DESIGN.md](DESIGN.md).
354
+
355
+ ---
356
+
357
+ ## Limitações conhecidas
358
+
359
+ - **Ditado avulso:** uma gravação = uma mensagem. Não há ditado contínuo/streaming nem leitura das
360
+ respostas em voz alta (TTS).
361
+ - **Sem escrita direta no input do prompt:** a V2 não expõe — a nenhum plugin — um meio de inserir
362
+ texto no campo do prompt. Isso foi verificado em três frentes: (1) a API `UI` não tem nada de
363
+ prompt/composer (nem na 2.0.29 nem na 2.0.32); (2) o OpenAPI do servidor não tem rota `/tui/*`;
364
+ (3) o evento interno `tui.prompt.append` é consumido pela TUI (`insertText` no composer), mas
365
+ **nada o publica** — não há endpoint HTTP nem superfície RPC. A V1 tinha
366
+ `client.tui.appendPrompt`, removido na V2. Por isso o texto passa pelo dialog de revisão
367
+ (já preenchido e editável), e o pedido de reabrir isso está em
368
+ [anomalyco/opencode#51209](https://github.com/anomalyco/opencode/issues/51209)
369
+ (veja também [#54681](https://github.com/anomalyco/opencode/issues/54681)). Quando a API existir,
370
+ trocar o dialog por "inserir no prompt" é uma mudança pequena e localizada.
371
+ - **Transcrição na nuvem:** o áudio é enviado ao OpenRouter e ao provedor escolhido. Não use com
372
+ informação sensível se isso for um problema.
373
+ - **Linux e macOS** foram validados pelos comandos e pela lógica, mas o teste ponta a ponta foi feito
374
+ apenas no Windows. Teste real em Linux: pendente.
375
+
376
+ ---
377
+
378
+ ## Segurança
379
+
380
+ - A chave vem da variável de ambiente ou, se ela não existir, do store do próprio OpenCode
381
+ (leitura somente-leitura). Nunca é logada, copiada nem persistida pelo plugin.
382
+ - O WAV é gravado em `os.tmpdir()` e apagado depois de transcrever (salvo `keepAudio: true`).
383
+ - Uma gravação em andamento é encerrada quando o plugin é descarregado.
384
+
385
+ ---
386
+
387
+ ## Licença
388
+
389
+ MIT — veja [LICENSE](LICENSE).
package/package.json CHANGED
@@ -1,6 +1,74 @@
1
1
  {
2
2
  "name": "opencode-voice-plugin",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Voice dictation for OpenCode V2: click a mic in the prompt footer, speak, and send the transcribed text.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./src/index.ts",
10
+ "types": "./src/index.ts"
11
+ },
12
+ "./tui": {
13
+ "import": "./src/tui.tsx",
14
+ "types": "./src/tui.tsx"
15
+ }
16
+ },
17
+ "files": [
18
+ "src",
19
+ "README.md",
20
+ "README.pt-BR.md",
21
+ "LICENSE"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/kabrau/opencode-voice-plugin.git"
26
+ },
27
+ "homepage": "https://github.com/kabrau/opencode-voice-plugin#readme",
28
+ "bugs": {
29
+ "url": "https://github.com/kabrau/opencode-voice-plugin/issues"
30
+ },
31
+ "author": "kabrau",
32
+ "keywords": [
33
+ "opencode",
34
+ "plugin",
35
+ "voice",
36
+ "dictation",
37
+ "speech-to-text",
38
+ "stt",
39
+ "whisper",
40
+ "openrouter",
41
+ "tui"
42
+ ],
43
+ "scripts": {
44
+ "typecheck": "tsc --noEmit",
45
+ "prepublishOnly": "npm run typecheck"
46
+ },
47
+ "dependencies": {
48
+ "@opencode/plugin": "2.0.29"
49
+ },
50
+ "peerDependencies": {
51
+ "@opentui/core": ">=0.5.17",
52
+ "@opentui/solid": ">=0.5.17",
53
+ "solid-js": ">=1.9.0"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "@opentui/core": {
57
+ "optional": true
58
+ },
59
+ "@opentui/solid": {
60
+ "optional": true
61
+ },
62
+ "solid-js": {
63
+ "optional": true
64
+ }
65
+ },
66
+ "devDependencies": {
67
+ "@opencode/theme": "2.0.29",
68
+ "@opentui/core": "0.6.6",
69
+ "@opentui/solid": "0.6.6",
70
+ "@types/node": "22.13.9",
71
+ "solid-js": "1.9.12",
72
+ "typescript": "5.9.3"
73
+ }
74
+ }