evo-cli 0.21.2__tar.gz → 0.22.0__tar.gz

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.
Files changed (120) hide show
  1. {evo_cli-0.21.2 → evo_cli-0.22.0}/PKG-INFO +29 -16
  2. {evo_cli-0.21.2 → evo_cli-0.22.0}/README.md +28 -15
  3. evo_cli-0.22.0/evo_cli/VERSION +1 -0
  4. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/opencode.py +4 -1
  5. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/tts.py +47 -30
  6. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/credentials/registry.py +11 -0
  7. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/tts/__init__.py +2 -0
  8. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/tts/core.py +32 -3
  9. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/tts/creds.py +19 -0
  10. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/tts/errors.py +1 -1
  11. evo_cli-0.22.0/evo_cli/tts/gemini.py +202 -0
  12. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/tts/http.py +3 -1
  13. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli.egg-info/PKG-INFO +29 -16
  14. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli.egg-info/SOURCES.txt +1 -0
  15. {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_opencode.py +13 -0
  16. {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_tts.py +147 -10
  17. evo_cli-0.21.2/evo_cli/VERSION +0 -1
  18. {evo_cli-0.21.2 → evo_cli-0.22.0}/Containerfile +0 -0
  19. {evo_cli-0.21.2 → evo_cli-0.22.0}/HISTORY.md +0 -0
  20. {evo_cli-0.21.2 → evo_cli-0.22.0}/LICENSE +0 -0
  21. {evo_cli-0.21.2 → evo_cli-0.22.0}/MANIFEST.in +0 -0
  22. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/__init__.py +0 -0
  23. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/__main__.py +0 -0
  24. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/base.py +0 -0
  25. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/cli.py +0 -0
  26. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/__init__.py +0 -0
  27. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/agent_toy.py +0 -0
  28. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/claude_code.py +0 -0
  29. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/cloudflare.py +0 -0
  30. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/cred.py +0 -0
  31. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/download.py +0 -0
  32. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/fix_claude.py +0 -0
  33. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/gdrive.py +0 -0
  34. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/gh.py +0 -0
  35. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/__init__.py +0 -0
  36. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/_dag.py +0 -0
  37. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/_git.py +0 -0
  38. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/_model.py +0 -0
  39. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/_mutate.py +0 -0
  40. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/_paths.py +0 -0
  41. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/_render.py +0 -0
  42. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/_server.py +0 -0
  43. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/check.py +0 -0
  44. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/clone.py +0 -0
  45. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/edit.py +0 -0
  46. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/pull.py +0 -0
  47. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/serve.py +0 -0
  48. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/show.py +0 -0
  49. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/views.py +0 -0
  50. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/web/assets/index-CsALFg16.css +0 -0
  51. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/web/assets/index-DwEXL5pV.js +0 -0
  52. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  53. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  54. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  55. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  56. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  57. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  58. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  59. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  60. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  61. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  62. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  63. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  64. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/web/index.html +0 -0
  65. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/hwid.py +0 -0
  66. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/hwid_reset.py +0 -0
  67. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/localproxy.py +0 -0
  68. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/mcp.py +0 -0
  69. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/miniconda.py +0 -0
  70. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/netcheck.py +0 -0
  71. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/plantuml.py +0 -0
  72. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/serp.py +0 -0
  73. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/site2s.py +0 -0
  74. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/ssh.py +0 -0
  75. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/sysmon.py +0 -0
  76. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/update.py +0 -0
  77. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/wifi.py +0 -0
  78. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/console.py +0 -0
  79. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/credentials/__init__.py +0 -0
  80. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/credentials/doctor.py +0 -0
  81. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/credentials/google_oauth.py +0 -0
  82. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/credentials/migrate.py +0 -0
  83. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/credentials/oauth_flow.py +0 -0
  84. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/credentials/store.py +0 -0
  85. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/credentials/sync.py +0 -0
  86. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/mcp_registry.py +0 -0
  87. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/serp/__init__.py +0 -0
  88. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/serp/client.py +0 -0
  89. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/serp/creds.py +0 -0
  90. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/serp/errors.py +0 -0
  91. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/serp/install.py +0 -0
  92. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/serp/render.py +0 -0
  93. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/tts/chunking.py +0 -0
  94. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/tts/openai.py +0 -0
  95. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/tts/player.py +0 -0
  96. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/tts/vbee.py +0 -0
  97. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli.egg-info/dependency_links.txt +0 -0
  98. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli.egg-info/entry_points.txt +0 -0
  99. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli.egg-info/requires.txt +0 -0
  100. {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli.egg-info/top_level.txt +0 -0
  101. {evo_cli-0.21.2 → evo_cli-0.22.0}/pyproject.toml +0 -0
  102. {evo_cli-0.21.2 → evo_cli-0.22.0}/setup.cfg +0 -0
  103. {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/__init__.py +0 -0
  104. {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_agent_toy.py +0 -0
  105. {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_claude_code.py +0 -0
  106. {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_cli.py +0 -0
  107. {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_console.py +0 -0
  108. {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_cred.py +0 -0
  109. {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_download.py +0 -0
  110. {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_fix_claude.py +0 -0
  111. {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_gh.py +0 -0
  112. {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_harness.py +0 -0
  113. {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_harness_clone.py +0 -0
  114. {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_harness_dag.py +0 -0
  115. {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_mcp.py +0 -0
  116. {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_plantuml.py +0 -0
  117. {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_serp.py +0 -0
  118. {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_sysmon.py +0 -0
  119. {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_update.py +0 -0
  120. {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_wifi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo_cli
3
- Version: 0.21.2
3
+ Version: 0.22.0
4
4
  Summary: Evolution CLI - a developer toolbox for setting up dev machines
5
5
  Author: maycuatroi
6
6
  Project-URL: Homepage, https://github.com/maycuatroi/evo-cli
@@ -195,53 +195,66 @@ uses `git pull --ff-only` so the command never creates merge commits.
195
195
 
196
196
  #### Text to Speech
197
197
 
198
- Synthesise speech through Vbee (Vietnamese) or OpenAI `gpt-4o-mini-tts`, and play it right away:
198
+ Synthesise speech through Gemini `gemini-3.1-flash-tts-preview`, Vbee (Vietnamese), or OpenAI
199
+ `gpt-4o-mini-tts`, and play it right away:
199
200
 
200
201
  ```bash
201
202
  evo tts speak "Xin chào, bản build đã xong"
202
- evo tts speak -f notes.md -o notes.mp3
203
+ evo tts speak -f notes.md -o notes.wav
204
+ evo tts speak "hôm nay trời đẹp" -V Sulafat --instructions "kể chuyện, ấm áp"
205
+ evo tts speak "[whispers] đừng nói với ai nhé"
203
206
  evo tts speak "hello there" -p openai -V nova --instructions "calm and encouraging"
204
207
  git log -1 --format=%s | evo tts speak
205
208
  ```
206
209
 
207
- `speak` is the realtime path. Text longer than the provider's per-request limit (Vbee 300
208
- characters, OpenAI 4000) is split on sentence boundaries and the audio is joined back into one file,
209
- so the first words start playing while the rest is still being synthesised.
210
+ `speak` is the realtime path. Text longer than the provider's per-request limit (Gemini 2000
211
+ characters, Vbee 300, OpenAI 4000) is split on sentence boundaries and the audio is joined back into
212
+ one file, so the first words start playing while the rest is still being synthesised.
213
+
214
+ Gemini is the most expressive of the three: 30 prebuilt voices, automatic language detection across
215
+ 70+ languages, free-form delivery notes through `--instructions`, and inline audio tags such as
216
+ `[whispers]`, `[excited]`, `[sighs]`, or `[very slow]` anywhere in the text. It answers with raw
217
+ 24 kHz PCM, so `--format wav` is the default there; `--format mp3` re-encodes through `ffmpeg` and
218
+ needs it on PATH. The format is also inferred from the `--output` suffix, so `-o notes.mp3` still
219
+ produces an mp3.
210
220
 
211
221
  For bulk work use the batch path, which goes through Vbee's async API and polls
212
222
  `/v1/tts/requests/{id}` until each audio link appears:
213
223
 
214
224
  ```bash
215
- evo tts batch chapters/ -o audio/ # one mp3 per .txt/.md file
225
+ evo tts batch chapters/ -o audio/ # one file per .txt/.md input
216
226
  evo tts batch a.txt b.txt -c 8 # 8 items in flight
217
227
  evo tts batch --manifest jobs.jsonl # {"id":.., "text":.., "voice":..} per line
218
228
  ```
219
229
 
220
- OpenAI has no batch speech endpoint, so with `-p openai` the items are parallelised locally instead.
230
+ Gemini and OpenAI have no batch speech endpoint, so there the items are parallelised locally instead.
221
231
 
222
- Voice codes come from `evo tts voices` (`-l en-US`, `--gender male`, `-p openai`, `--json`).
232
+ Voice codes come from `evo tts voices` (`-p gemini`, `-p openai`, `-l en-US`, `--gender male`,
233
+ `--json`).
223
234
 
224
235
  Credentials live in the omelet store, never in flags or source:
225
236
 
226
237
  ```bash
238
+ evo cred add gemini_api_key --from-stdin # key from https://aistudio.google.com/apikey
227
239
  evo cred add vbee.app_id --from-stdin # UUID from https://studio.vbee.vn/apps
228
240
  evo cred add vbee.token --from-stdin # JWT from the same app page
229
241
  evo cred add openai_api_key --from-stdin
230
242
  ```
231
243
 
232
- `VBEE_APP_ID`, `VBEE_TOKEN`, and `OPENAI_API_KEY` override the store when set.
244
+ `GEMINI_API_KEY` (or `GOOGLE_API_KEY`), `VBEE_APP_ID`, `VBEE_TOKEN`, and `OPENAI_API_KEY` override
245
+ the store when set.
233
246
 
234
247
  `--provider auto` (the default) resolves to `EVO_TTS_PROVIDER` when that is set, and otherwise to
235
- whichever provider has credentials. Pick a machine default once:
248
+ the first of Gemini, Vbee, OpenAI that has credentials. Pick a machine default once:
236
249
 
237
250
  ```bash
238
- export EVO_TTS_PROVIDER=openai # what `auto` means here
239
- export EVO_TTS_VOICE_OPENAI=nova # default voice for that provider only
251
+ export EVO_TTS_PROVIDER=gemini # what `auto` means here
252
+ export EVO_TTS_VOICE_GEMINI=Sulafat # default voice for that provider only
240
253
  ```
241
254
 
242
- Prefer the provider-scoped `EVO_TTS_VOICE_OPENAI` / `EVO_TTS_VOICE_VBEE` over a bare
243
- `EVO_TTS_VOICE`: a shared value breaks as soon as you pass `--provider vbee`, because an OpenAI
244
- voice name is not a Vbee voice code.
255
+ Prefer the provider-scoped `EVO_TTS_VOICE_GEMINI` / `EVO_TTS_VOICE_OPENAI` / `EVO_TTS_VOICE_VBEE`
256
+ over a bare `EVO_TTS_VOICE`: a shared value breaks as soon as you pass `--provider vbee`, because a
257
+ Gemini voice name is not a Vbee voice code.
245
258
 
246
259
  Playback uses whichever of `ffplay`, `mpv`, `cvlc`, `afplay`, or `paplay`/`aplay` is on PATH, and
247
260
  falls back to PowerShell's `MediaPlayer` on Windows. Without any of them the audio is still written
@@ -167,53 +167,66 @@ uses `git pull --ff-only` so the command never creates merge commits.
167
167
 
168
168
  #### Text to Speech
169
169
 
170
- Synthesise speech through Vbee (Vietnamese) or OpenAI `gpt-4o-mini-tts`, and play it right away:
170
+ Synthesise speech through Gemini `gemini-3.1-flash-tts-preview`, Vbee (Vietnamese), or OpenAI
171
+ `gpt-4o-mini-tts`, and play it right away:
171
172
 
172
173
  ```bash
173
174
  evo tts speak "Xin chào, bản build đã xong"
174
- evo tts speak -f notes.md -o notes.mp3
175
+ evo tts speak -f notes.md -o notes.wav
176
+ evo tts speak "hôm nay trời đẹp" -V Sulafat --instructions "kể chuyện, ấm áp"
177
+ evo tts speak "[whispers] đừng nói với ai nhé"
175
178
  evo tts speak "hello there" -p openai -V nova --instructions "calm and encouraging"
176
179
  git log -1 --format=%s | evo tts speak
177
180
  ```
178
181
 
179
- `speak` is the realtime path. Text longer than the provider's per-request limit (Vbee 300
180
- characters, OpenAI 4000) is split on sentence boundaries and the audio is joined back into one file,
181
- so the first words start playing while the rest is still being synthesised.
182
+ `speak` is the realtime path. Text longer than the provider's per-request limit (Gemini 2000
183
+ characters, Vbee 300, OpenAI 4000) is split on sentence boundaries and the audio is joined back into
184
+ one file, so the first words start playing while the rest is still being synthesised.
185
+
186
+ Gemini is the most expressive of the three: 30 prebuilt voices, automatic language detection across
187
+ 70+ languages, free-form delivery notes through `--instructions`, and inline audio tags such as
188
+ `[whispers]`, `[excited]`, `[sighs]`, or `[very slow]` anywhere in the text. It answers with raw
189
+ 24 kHz PCM, so `--format wav` is the default there; `--format mp3` re-encodes through `ffmpeg` and
190
+ needs it on PATH. The format is also inferred from the `--output` suffix, so `-o notes.mp3` still
191
+ produces an mp3.
182
192
 
183
193
  For bulk work use the batch path, which goes through Vbee's async API and polls
184
194
  `/v1/tts/requests/{id}` until each audio link appears:
185
195
 
186
196
  ```bash
187
- evo tts batch chapters/ -o audio/ # one mp3 per .txt/.md file
197
+ evo tts batch chapters/ -o audio/ # one file per .txt/.md input
188
198
  evo tts batch a.txt b.txt -c 8 # 8 items in flight
189
199
  evo tts batch --manifest jobs.jsonl # {"id":.., "text":.., "voice":..} per line
190
200
  ```
191
201
 
192
- OpenAI has no batch speech endpoint, so with `-p openai` the items are parallelised locally instead.
202
+ Gemini and OpenAI have no batch speech endpoint, so there the items are parallelised locally instead.
193
203
 
194
- Voice codes come from `evo tts voices` (`-l en-US`, `--gender male`, `-p openai`, `--json`).
204
+ Voice codes come from `evo tts voices` (`-p gemini`, `-p openai`, `-l en-US`, `--gender male`,
205
+ `--json`).
195
206
 
196
207
  Credentials live in the omelet store, never in flags or source:
197
208
 
198
209
  ```bash
210
+ evo cred add gemini_api_key --from-stdin # key from https://aistudio.google.com/apikey
199
211
  evo cred add vbee.app_id --from-stdin # UUID from https://studio.vbee.vn/apps
200
212
  evo cred add vbee.token --from-stdin # JWT from the same app page
201
213
  evo cred add openai_api_key --from-stdin
202
214
  ```
203
215
 
204
- `VBEE_APP_ID`, `VBEE_TOKEN`, and `OPENAI_API_KEY` override the store when set.
216
+ `GEMINI_API_KEY` (or `GOOGLE_API_KEY`), `VBEE_APP_ID`, `VBEE_TOKEN`, and `OPENAI_API_KEY` override
217
+ the store when set.
205
218
 
206
219
  `--provider auto` (the default) resolves to `EVO_TTS_PROVIDER` when that is set, and otherwise to
207
- whichever provider has credentials. Pick a machine default once:
220
+ the first of Gemini, Vbee, OpenAI that has credentials. Pick a machine default once:
208
221
 
209
222
  ```bash
210
- export EVO_TTS_PROVIDER=openai # what `auto` means here
211
- export EVO_TTS_VOICE_OPENAI=nova # default voice for that provider only
223
+ export EVO_TTS_PROVIDER=gemini # what `auto` means here
224
+ export EVO_TTS_VOICE_GEMINI=Sulafat # default voice for that provider only
212
225
  ```
213
226
 
214
- Prefer the provider-scoped `EVO_TTS_VOICE_OPENAI` / `EVO_TTS_VOICE_VBEE` over a bare
215
- `EVO_TTS_VOICE`: a shared value breaks as soon as you pass `--provider vbee`, because an OpenAI
216
- voice name is not a Vbee voice code.
227
+ Prefer the provider-scoped `EVO_TTS_VOICE_GEMINI` / `EVO_TTS_VOICE_OPENAI` / `EVO_TTS_VOICE_VBEE`
228
+ over a bare `EVO_TTS_VOICE`: a shared value breaks as soon as you pass `--provider vbee`, because a
229
+ Gemini voice name is not a Vbee voice code.
217
230
 
218
231
  Playback uses whichever of `ffplay`, `mpv`, `cvlc`, `afplay`, or `paplay`/`aplay` is on PATH, and
219
232
  falls back to PowerShell's `MediaPlayer` on Windows. Without any of them the audio is still written
@@ -0,0 +1 @@
1
+ 0.22.0
@@ -447,10 +447,13 @@ def configure_opencode_project(project_path):
447
447
  def verify_mcp_servers():
448
448
  """Run a basic JSON-RPC initialize check against installed MCP servers."""
449
449
  step("Verifying MCP servers")
450
+ # MCP frames stdio messages one per line, so without the trailing newline the
451
+ # server sees an unterminated frame, exits 0 on EOF and answers nothing -
452
+ # which read as "server broken" for every server we ever verified.
450
453
  init_message = (
451
454
  '{"jsonrpc":"2.0","id":1,"method":"initialize",'
452
455
  '"params":{"protocolVersion":"2024-11-05","capabilities":{},'
453
- '"clientInfo":{"name":"evo-cli","version":"1.0"}}}'
456
+ '"clientInfo":{"name":"evo-cli","version":"1.0"}}}\n'
454
457
  )
455
458
  for name, cmd in _local_mcp_commands():
456
459
  try:
@@ -16,10 +16,12 @@ TEXT_SUFFIXES = (".txt", ".md")
16
16
  SPEAK_EPILOG = Text.from_markup(
17
17
  "[bold]Examples[/bold]\n\n"
18
18
  " [cyan]evo tts speak 'Xin chào, bản build đã xong'[/cyan] speak it out loud now\n"
19
- " [cyan]evo tts speak -f notes.md -o notes.mp3[/cyan] read a file, keep the audio\n"
19
+ " [cyan]evo tts speak -f notes.md -o notes.wav[/cyan] read a file, keep the audio\n"
20
+ " [cyan]evo tts speak 'hi' -V Sulafat --instructions 'kể chuyện, ấm áp'[/cyan] steer the delivery\n"
21
+ " [cyan]evo tts speak '[whispers] bí mật nhé'[/cyan] Gemini audio tags work inline\n"
20
22
  " [cyan]evo tts speak 'hello' -p openai -V nova[/cyan] use gpt-4o-mini-tts instead\n"
21
23
  " [cyan]git log -1 --format=%s | evo tts speak[/cyan] read stdin\n"
22
- " [cyan]evo tts speak 'hi' --stdout > out.mp3[/cyan] pipe raw audio"
24
+ " [cyan]evo tts speak 'hi' --stdout > out.wav[/cyan] pipe raw audio"
23
25
  )
24
26
 
25
27
  BATCH_EPILOG = Text.from_markup(
@@ -33,8 +35,9 @@ BATCH_EPILOG = Text.from_markup(
33
35
 
34
36
  VOICES_EPILOG = Text.from_markup(
35
37
  "[bold]Examples[/bold]\n\n"
36
- " [cyan]evo tts voices[/cyan] Vbee Vietnamese voices\n"
37
- " [cyan]evo tts voices -l en-US --gender male[/cyan] filter by language and gender\n"
38
+ " [cyan]evo tts voices[/cyan] voices of the auto-picked provider\n"
39
+ " [cyan]evo tts voices -p gemini[/cyan] the 30 Gemini prebuilt voices\n"
40
+ " [cyan]evo tts voices -p vbee -l en-US --gender male[/cyan] filter by language and gender\n"
38
41
  " [cyan]evo tts voices -p openai[/cyan] gpt-4o-mini-tts voices\n"
39
42
  " [cyan]evo tts voices --json[/cyan] machine-readable"
40
43
  )
@@ -90,6 +93,16 @@ def collect_items(inputs, texts, manifest):
90
93
  return [item for item in items if item["text"].strip()]
91
94
 
92
95
 
96
+ def resolve_output_format(provider, output_format, output=None):
97
+ if output_format:
98
+ return output_format
99
+ if output:
100
+ suffix = Path(output).suffix.lstrip(".").lower()
101
+ if suffix in core.supported_formats(provider):
102
+ return suffix
103
+ return core.default_format(provider)
104
+
105
+
93
106
  def unique_path(out_dir, name, output_format):
94
107
  candidate = out_dir / f"{name}.{output_format}"
95
108
  counter = 2
@@ -101,12 +114,16 @@ def unique_path(out_dir, name, output_format):
101
114
 
102
115
  @click.group("tts")
103
116
  def tts_group():
104
- """**Text to speech** via Vbee (Vietnamese) or OpenAI `gpt-4o-mini-tts`.
117
+ """**Text to speech** via Gemini `gemini-3.1-flash-tts-preview`, Vbee, or OpenAI `gpt-4o-mini-tts`.
105
118
 
106
119
  `speak` is the realtime path: it synthesises and plays immediately.
107
120
  `batch` is the bulk path: it uses Vbee's async API and writes one file per input.
108
- Credentials come from the omelet store (`evo cred add vbee.app_id`, `vbee.token`,
109
- `openai_api_key`); nothing is read from hardcoded values.
121
+ Credentials come from the omelet store (`evo cred add gemini_api_key`, `vbee.app_id`,
122
+ `vbee.token`, `openai_api_key`); nothing is read from hardcoded values.
123
+
124
+ Gemini is the default when its key is stored: 30 expressive voices, any of the
125
+ 70+ supported languages, plus inline audio tags like `[whispers]` or `[excited]`
126
+ and free-form delivery notes through `--instructions`.
110
127
  """
111
128
 
112
129
 
@@ -116,25 +133,24 @@ def tts_group():
116
133
  @click.option(
117
134
  "-p",
118
135
  "--provider",
119
- type=click.Choice(["auto", "vbee", "openai"]),
136
+ type=click.Choice(["auto", "gemini", "vbee", "openai"]),
120
137
  default="auto",
121
138
  show_default=True,
122
- help="auto picks Vbee when its credentials exist, else OpenAI.",
139
+ help="auto picks Gemini when its key exists, else Vbee, else OpenAI.",
123
140
  )
124
141
  @click.option("-V", "--voice", help="Voice code (see `evo tts voices`).")
125
142
  @click.option("-o", "--output", help="Keep the audio at this path instead of a temp file.")
126
143
  @click.option(
127
144
  "--format",
128
145
  "output_format",
129
- type=click.Choice(["mp3", "wav"]),
130
- default="mp3",
131
- show_default=True,
132
- help="Audio container.",
146
+ type=click.Choice(["mp3", "wav", "pcm"]),
147
+ default=None,
148
+ help="Audio container. Default: wav on Gemini, mp3 elsewhere, or the suffix of --output.",
133
149
  )
134
150
  @click.option("--speed", type=float, default=1.0, show_default=True, help="Speaking rate (Vbee: 0.25-1.9).")
135
- @click.option("--bitrate", type=int, default=128, show_default=True, help="Vbee bitrate in kbps.")
136
- @click.option("--instructions", help="OpenAI only: how the voice should deliver the text.")
137
- @click.option("--model", help="OpenAI model override (default gpt-4o-mini-tts).")
151
+ @click.option("--bitrate", type=int, default=128, show_default=True, help="Bitrate in kbps (Vbee, Gemini mp3).")
152
+ @click.option("--instructions", help="Gemini/OpenAI: how the voice should deliver the text.")
153
+ @click.option("--model", help="Model override (default gemini-3.1-flash-tts-preview / gpt-4o-mini-tts).")
138
154
  @click.option("--no-play", is_flag=True, help="Synthesise only; do not play through the speakers.")
139
155
  @click.option("--stdout", "to_stdout", is_flag=True, help="Write raw audio bytes to stdout (implies --no-play).")
140
156
  @click.option("-q", "--quiet", is_flag=True, help="Suppress progress output.")
@@ -156,14 +172,15 @@ def speak(
156
172
  """Synthesise **TEXT** and play it right away.
157
173
 
158
174
  Long text is split on sentence boundaries so each request stays inside the
159
- provider's realtime limit (Vbee 300 characters, OpenAI 4000), then the
160
- resulting audio is joined back into one file.
175
+ provider's realtime limit (Gemini 2000 characters, Vbee 300, OpenAI 4000),
176
+ then the resulting audio is joined back into one file.
161
177
  """
162
178
  body = read_input_text(text, text_file)
163
179
  if not quiet and not to_stdout:
164
180
  step("evo tts speak")
165
181
  try:
166
182
  resolved = core.resolve_provider(provider)
183
+ output_format = resolve_output_format(resolved, output_format, output)
167
184
  chunks = core.chunk_limit(resolved, "realtime")
168
185
  if not quiet and not to_stdout:
169
186
  info(
@@ -221,10 +238,10 @@ def speak(
221
238
  @click.option(
222
239
  "-p",
223
240
  "--provider",
224
- type=click.Choice(["auto", "vbee", "openai"]),
241
+ type=click.Choice(["auto", "gemini", "vbee", "openai"]),
225
242
  default="auto",
226
243
  show_default=True,
227
- help="auto picks Vbee when its credentials exist, else OpenAI.",
244
+ help="auto picks Gemini when its key exists, else Vbee, else OpenAI.",
228
245
  )
229
246
  @click.option(
230
247
  "--mode",
@@ -237,15 +254,14 @@ def speak(
237
254
  @click.option(
238
255
  "--format",
239
256
  "output_format",
240
- type=click.Choice(["mp3", "wav"]),
241
- default="mp3",
242
- show_default=True,
243
- help="Audio container.",
257
+ type=click.Choice(["mp3", "wav", "pcm"]),
258
+ default=None,
259
+ help="Audio container. Default: wav on Gemini, mp3 elsewhere.",
244
260
  )
245
261
  @click.option("--speed", type=float, default=1.0, show_default=True, help="Speaking rate.")
246
- @click.option("--bitrate", type=int, default=128, show_default=True, help="Vbee bitrate in kbps.")
247
- @click.option("--instructions", help="OpenAI only: how the voice should deliver the text.")
248
- @click.option("--model", help="OpenAI model override (default gpt-4o-mini-tts).")
262
+ @click.option("--bitrate", type=int, default=128, show_default=True, help="Bitrate in kbps (Vbee, Gemini mp3).")
263
+ @click.option("--instructions", help="Gemini/OpenAI: how the voice should deliver the text.")
264
+ @click.option("--model", help="Model override (default gemini-3.1-flash-tts-preview / gpt-4o-mini-tts).")
249
265
  @click.option("-c", "--concurrency", type=int, default=4, show_default=True, help="Items in flight at once.")
250
266
  @click.option("--webhook", help="Vbee webhookUrl; the API requires one even though evo polls for the result.")
251
267
  @click.option("--timeout", type=int, default=900, show_default=True, help="Seconds to wait per async request.")
@@ -287,9 +303,10 @@ def batch(
287
303
  error(str(exc))
288
304
  sys.exit(1)
289
305
 
306
+ output_format = resolve_output_format(resolved, output_format)
290
307
  effective_mode = mode
291
- if resolved == "openai" and mode == "batch":
292
- info("OpenAI has no batch speech endpoint - running the items concurrently instead.")
308
+ if resolved in ("openai", "gemini") and mode == "batch":
309
+ info(f"{resolved} has no batch speech endpoint - running the items concurrently instead.")
293
310
  effective_mode = "realtime"
294
311
 
295
312
  target_dir = Path(out_dir)
@@ -340,7 +357,7 @@ def batch(
340
357
  @click.option(
341
358
  "-p",
342
359
  "--provider",
343
- type=click.Choice(["auto", "vbee", "openai"]),
360
+ type=click.Choice(["auto", "gemini", "vbee", "openai"]),
344
361
  default="auto",
345
362
  show_default=True,
346
363
  help="Which catalog to list.",
@@ -71,6 +71,17 @@ SPECS = [
71
71
  "rotate": "https://studio.vbee.vn/apps -> open the app -> copy App ID + Token",
72
72
  "keys": ["vbee"],
73
73
  },
74
+ {
75
+ "path": "ai/gemini.json",
76
+ "id": "gemini",
77
+ "service": "Google Gemini API",
78
+ "category": "ai",
79
+ "type": "api_key",
80
+ "lifetime": "stable",
81
+ "description": "Gemini API key (evo tts gemini provider, generateContent)",
82
+ "rotate": "https://aistudio.google.com/apikey -> create or rotate the key",
83
+ "keys": ["gemini_api_key"],
84
+ },
74
85
  {
75
86
  "path": "ai/google.json",
76
87
  "id": "google_api",
@@ -2,6 +2,7 @@ from evo_cli.tts.core import (
2
2
  MODES,
3
3
  PROVIDERS,
4
4
  chunk_limit,
5
+ default_format,
5
6
  default_voice,
6
7
  default_voice_for,
7
8
  list_voices,
@@ -18,6 +19,7 @@ __all__ = [
18
19
  "PROVIDERS",
19
20
  "TtsError",
20
21
  "chunk_limit",
22
+ "default_format",
21
23
  "default_voice",
22
24
  "default_voice_for",
23
25
  "list_voices",
@@ -1,13 +1,14 @@
1
1
  import os
2
2
  from concurrent.futures import ThreadPoolExecutor, as_completed
3
3
 
4
+ from evo_cli.tts import gemini as gemini_tts
4
5
  from evo_cli.tts import openai as openai_tts
5
6
  from evo_cli.tts import vbee
6
7
  from evo_cli.tts.chunking import join_audio, split_text
7
- from evo_cli.tts.creds import has_openai_credentials, has_vbee_credentials
8
+ from evo_cli.tts.creds import has_gemini_credentials, has_openai_credentials, has_vbee_credentials
8
9
  from evo_cli.tts.errors import TtsError
9
10
 
10
- PROVIDERS = ("vbee", "openai")
11
+ PROVIDERS = ("gemini", "vbee", "openai")
11
12
  JOINABLE_FORMATS = ("mp3", "wav", "pcm")
12
13
  MODES = ("realtime", "batch")
13
14
 
@@ -21,12 +22,15 @@ def resolve_provider(provider):
21
22
  return preferred
22
23
  if preferred and preferred != "auto":
23
24
  raise TtsError(f"EVO_TTS_PROVIDER is set to '{preferred}', which is not one of: {', '.join(PROVIDERS)}")
25
+ if has_gemini_credentials():
26
+ return "gemini"
24
27
  if has_vbee_credentials():
25
28
  return "vbee"
26
29
  if has_openai_credentials():
27
30
  return "openai"
28
31
  raise TtsError(
29
- "no TTS credentials found. Store Vbee with "
32
+ "no TTS credentials found. Store Gemini with "
33
+ "`evo cred add gemini_api_key --from-stdin`, Vbee with "
30
34
  "`evo cred add vbee.app_id --from-stdin` + `evo cred add vbee.token --from-stdin`, "
31
35
  "or OpenAI with `evo cred add openai_api_key --from-stdin`."
32
36
  )
@@ -42,14 +46,25 @@ def default_voice_for(provider):
42
46
 
43
47
 
44
48
  def default_voice(provider):
49
+ if provider == "gemini":
50
+ return gemini_tts.DEFAULT_VOICE
45
51
  return vbee.DEFAULT_VOICE if provider == "vbee" else openai_tts.DEFAULT_VOICE
46
52
 
47
53
 
48
54
  def supported_formats(provider):
55
+ if provider == "gemini":
56
+ return gemini_tts.FORMATS
49
57
  return vbee.FORMATS if provider == "vbee" else openai_tts.FORMATS
50
58
 
51
59
 
60
+ def default_format(provider):
61
+ # Gemini only ever returns raw PCM, so wav is the format that needs no extra tooling.
62
+ return gemini_tts.DEFAULT_FORMAT if provider == "gemini" else "mp3"
63
+
64
+
52
65
  def chunk_limit(provider, mode):
66
+ if provider == "gemini":
67
+ return gemini_tts.TEXT_LIMIT
53
68
  if provider == "vbee":
54
69
  return vbee.BATCH_LIMIT if mode == "batch" else vbee.REALTIME_LIMIT
55
70
  return openai_tts.TEXT_LIMIT
@@ -136,6 +151,18 @@ def synthesize(
136
151
  sample_rate=sample_rate,
137
152
  )
138
153
 
154
+ elif provider == "gemini":
155
+
156
+ def call(chunk):
157
+ return gemini_tts.synthesize(
158
+ chunk,
159
+ voice=voice,
160
+ output_format=output_format,
161
+ model=model or gemini_tts.DEFAULT_MODEL,
162
+ instructions=instructions,
163
+ bitrate=bitrate,
164
+ )
165
+
139
166
  else:
140
167
 
141
168
  def call(chunk):
@@ -198,6 +225,8 @@ def synthesize_many(items, concurrency=4, on_item=None, **kwargs):
198
225
 
199
226
  def list_voices(provider="auto", language_code=None, gender=None, ownership="VBEE", limit=100):
200
227
  provider = resolve_provider(provider)
228
+ if provider == "gemini":
229
+ return gemini_tts.list_voices()
201
230
  if provider == "openai":
202
231
  return openai_tts.list_voices()
203
232
  voices, pagination = vbee.list_voices(
@@ -55,5 +55,24 @@ def has_openai_credentials():
55
55
  return True
56
56
 
57
57
 
58
+ def gemini_api_key():
59
+ key = _resolve("GEMINI_API_KEY", "gemini_api_key") or os.environ.get("GOOGLE_API_KEY")
60
+ if not key:
61
+ raise TtsError(
62
+ "missing Gemini credentials: gemini_api_key\n"
63
+ "Get one at https://aistudio.google.com/apikey, then store it with:\n"
64
+ " evo cred add gemini_api_key --from-stdin"
65
+ )
66
+ return key
67
+
68
+
69
+ def has_gemini_credentials():
70
+ try:
71
+ gemini_api_key()
72
+ except TtsError:
73
+ return False
74
+ return True
75
+
76
+
58
77
  def vbee_webhook_url():
59
78
  return _resolve("VBEE_WEBHOOK_URL", "vbee.webhook_url")
@@ -1,2 +1,2 @@
1
1
  class TtsError(RuntimeError):
2
- pass
2
+ status = None