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.
- {evo_cli-0.21.2 → evo_cli-0.22.0}/PKG-INFO +29 -16
- {evo_cli-0.21.2 → evo_cli-0.22.0}/README.md +28 -15
- evo_cli-0.22.0/evo_cli/VERSION +1 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/opencode.py +4 -1
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/tts.py +47 -30
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/credentials/registry.py +11 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/tts/__init__.py +2 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/tts/core.py +32 -3
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/tts/creds.py +19 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/tts/errors.py +1 -1
- evo_cli-0.22.0/evo_cli/tts/gemini.py +202 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/tts/http.py +3 -1
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli.egg-info/PKG-INFO +29 -16
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli.egg-info/SOURCES.txt +1 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_opencode.py +13 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_tts.py +147 -10
- evo_cli-0.21.2/evo_cli/VERSION +0 -1
- {evo_cli-0.21.2 → evo_cli-0.22.0}/Containerfile +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/HISTORY.md +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/LICENSE +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/MANIFEST.in +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/__init__.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/__main__.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/base.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/cli.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/__init__.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/agent_toy.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/claude_code.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/cloudflare.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/cred.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/download.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/fix_claude.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/gdrive.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/gh.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/__init__.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/_dag.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/_git.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/_model.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/_mutate.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/_paths.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/_render.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/_server.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/check.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/clone.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/edit.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/pull.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/serve.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/show.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/views.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/web/assets/index-CsALFg16.css +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/web/assets/index-DwEXL5pV.js +0 -0
- {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
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- {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
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- {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
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- {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
- {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
- {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
- {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
- {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
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/harness/web/index.html +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/hwid.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/hwid_reset.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/localproxy.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/mcp.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/miniconda.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/netcheck.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/plantuml.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/serp.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/site2s.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/ssh.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/sysmon.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/update.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/commands/wifi.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/console.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/credentials/__init__.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/credentials/doctor.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/credentials/google_oauth.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/credentials/migrate.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/credentials/oauth_flow.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/credentials/store.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/credentials/sync.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/mcp_registry.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/serp/__init__.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/serp/client.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/serp/creds.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/serp/errors.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/serp/install.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/serp/render.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/tts/chunking.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/tts/openai.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/tts/player.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli/tts/vbee.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli.egg-info/dependency_links.txt +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli.egg-info/entry_points.txt +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli.egg-info/requires.txt +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/evo_cli.egg-info/top_level.txt +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/pyproject.toml +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/setup.cfg +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/__init__.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_agent_toy.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_claude_code.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_cli.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_console.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_cred.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_download.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_fix_claude.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_gh.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_harness.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_harness_clone.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_harness_dag.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_mcp.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_plantuml.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_serp.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_sysmon.py +0 -0
- {evo_cli-0.21.2 → evo_cli-0.22.0}/tests/test_update.py +0 -0
- {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.
|
|
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
|
|
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.
|
|
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 (
|
|
208
|
-
characters, OpenAI 4000) is split on sentence boundaries and the audio is joined back into
|
|
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
|
|
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
|
|
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`,
|
|
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
|
|
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
|
-
|
|
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=
|
|
239
|
-
export
|
|
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`
|
|
243
|
-
`EVO_TTS_VOICE`: a shared value breaks as soon as you pass `--provider vbee`, because
|
|
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
|
|
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.
|
|
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 (
|
|
180
|
-
characters, OpenAI 4000) is split on sentence boundaries and the audio is joined back into
|
|
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
|
|
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
|
|
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`,
|
|
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
|
|
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
|
-
|
|
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=
|
|
211
|
-
export
|
|
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`
|
|
215
|
-
`EVO_TTS_VOICE`: a shared value breaks as soon as you pass `--provider vbee`, because
|
|
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.
|
|
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.
|
|
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]
|
|
37
|
-
" [cyan]evo tts voices -
|
|
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
|
|
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
|
|
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
|
|
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=
|
|
131
|
-
|
|
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="
|
|
136
|
-
@click.option("--instructions", help="OpenAI
|
|
137
|
-
@click.option("--model", help="
|
|
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 (
|
|
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
|
|
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=
|
|
242
|
-
|
|
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="
|
|
247
|
-
@click.option("--instructions", help="OpenAI
|
|
248
|
-
@click.option("--model", help="
|
|
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
|
|
292
|
-
info("
|
|
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
|
|
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
|
-
|
|
2
|
+
status = None
|