everypixel-cli 0.3.0__tar.gz → 0.4.1__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 (25) hide show
  1. {everypixel_cli-0.3.0 → everypixel_cli-0.4.1}/CHANGELOG.md +10 -0
  2. {everypixel_cli-0.3.0 → everypixel_cli-0.4.1}/PKG-INFO +32 -4
  3. {everypixel_cli-0.3.0 → everypixel_cli-0.4.1}/README.md +31 -3
  4. everypixel_cli-0.4.1/src/everypixel_cli/__init__.py +1 -0
  5. {everypixel_cli-0.3.0 → everypixel_cli-0.4.1}/src/everypixel_cli/application/serialization.py +2 -0
  6. {everypixel_cli-0.3.0 → everypixel_cli-0.4.1}/src/everypixel_cli/application/services.py +184 -63
  7. {everypixel_cli-0.3.0 → everypixel_cli-0.4.1}/src/everypixel_cli/cli.py +86 -18
  8. {everypixel_cli-0.3.0 → everypixel_cli-0.4.1}/src/everypixel_cli/mcp_server.py +65 -19
  9. {everypixel_cli-0.3.0 → everypixel_cli-0.4.1}/src/everypixel_cli/output.py +24 -0
  10. {everypixel_cli-0.3.0 → everypixel_cli-0.4.1}/src/everypixel_cli/resources/openapi.json +406 -40
  11. {everypixel_cli-0.3.0 → everypixel_cli-0.4.1}/src/everypixel_cli/schemas.py +287 -1
  12. everypixel_cli-0.3.0/src/everypixel_cli/__init__.py +0 -1
  13. {everypixel_cli-0.3.0 → everypixel_cli-0.4.1}/.gitignore +0 -0
  14. {everypixel_cli-0.3.0 → everypixel_cli-0.4.1}/LICENSE +0 -0
  15. {everypixel_cli-0.3.0 → everypixel_cli-0.4.1}/pyproject.toml +0 -0
  16. {everypixel_cli-0.3.0 → everypixel_cli-0.4.1}/src/everypixel_cli/__main__.py +0 -0
  17. {everypixel_cli-0.3.0 → everypixel_cli-0.4.1}/src/everypixel_cli/application/__init__.py +0 -0
  18. {everypixel_cli-0.3.0 → everypixel_cli-0.4.1}/src/everypixel_cli/application/chat.py +0 -0
  19. {everypixel_cli-0.3.0 → everypixel_cli-0.4.1}/src/everypixel_cli/application/models.py +0 -0
  20. {everypixel_cli-0.3.0 → everypixel_cli-0.4.1}/src/everypixel_cli/client.py +0 -0
  21. {everypixel_cli-0.3.0 → everypixel_cli-0.4.1}/src/everypixel_cli/config.py +0 -0
  22. {everypixel_cli-0.3.0 → everypixel_cli-0.4.1}/src/everypixel_cli/errors.py +0 -0
  23. {everypixel_cli-0.3.0 → everypixel_cli-0.4.1}/src/everypixel_cli/files.py +0 -0
  24. {everypixel_cli-0.3.0 → everypixel_cli-0.4.1}/src/everypixel_cli/openapi.py +0 -0
  25. {everypixel_cli-0.3.0 → everypixel_cli-0.4.1}/src/everypixel_cli/resources/__init__.py +0 -0
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.1] - 2026-10-08
4
+
5
+ - Display estimated and billed generation costs, including chat responses.
6
+ - Report observed queue time while waiting for tasks.
7
+
8
+ ## [0.4.0] - 2026-10-07
9
+
10
+ - Add ElevenLabs speech synthesis and voice design.
11
+ - Add saved voice creation, listing, deletion, and Qwen cloning by voice ID.
12
+
3
13
  ## [0.3.0] - 2026-09-18
4
14
 
5
15
  - Add GPT Image 2.5 Sunburst and quality selection for GPT Image models.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: everypixel-cli
3
- Version: 0.3.0
3
+ Version: 0.4.1
4
4
  Summary: Official command line interface for Everypixel API
5
5
  Project-URL: Homepage, https://labs.everypixel.com
6
6
  Project-URL: Repository, https://github.com/everypixel/everypixel-cli
@@ -263,9 +263,12 @@ They support 4K output. The default `seedvr2` supports up to 1440p, with a
263
263
  | `everypixel lipsync video` | Synchronize a video with an audio track |
264
264
  | `everypixel lipsync image` | Create a lipsync video from a still image and audio |
265
265
  | `everypixel audio transcribe` | Transcribe speech from an audio file |
266
- | `everypixel audio tts-create` | Create speech with a built-in speaker |
267
- | `everypixel audio tts-clone` | Create speech from a cloned voice sample |
268
- | `everypixel audio tts-voice` | Create speech with a character voice |
266
+ | `everypixel audio tts-create` | Create speech with a Qwen preset or saved ElevenLabs voice |
267
+ | `everypixel audio tts-clone` | Create Qwen speech from an audio sample or saved voice |
268
+ | `everypixel audio tts-design` | Design voice previews from a description |
269
+ | `everypixel audio tts-voice` | Save audio samples or a design preview as a voice |
270
+ | `everypixel audio tts-voices` | List presets and saved voices |
271
+ | `everypixel audio tts-delete` | Delete an owned saved voice |
269
272
 
270
273
  Lipsync supports 720p; the API checks source media dimensions and duration.
271
274
 
@@ -290,6 +293,25 @@ everypixel audio tts-create \
290
293
  --download ./outputs
291
294
  ```
292
295
 
296
+ `tts-create` lets the API choose the model when `--model` is omitted.
297
+ `tts-clone` and `tts-design` default to `--model qwen3`.
298
+ `--speaker`, `--style`, `--language`, and `--seed` are Qwen controls; `--character` applies only to Qwen voice design.
299
+ Qwen text is limited to 200 characters. ElevenLabs synthesis accepts up to
300
+ 2048 characters and requires `--voice-id`. Supported synthesis models are
301
+ `eleven_v4`, `eleven_v4_turbo`, `eleven_v3`, `eleven_v3_conversational`,
302
+ `eleven_multilingual_v2`, and `eleven_flash_v2_5`. ElevenLabs design uses
303
+ `eleven_multilingual_ttv_v2` or `eleven_ttv_v3`, with 100–1000 characters of
304
+ text and a 20–1000 character `--prompt`.
305
+
306
+ Save a returned preview with `tts-voice --name "Narrator" --preview-id UUID`, or save local
307
+ samples with `tts-voice --name "My voice" --provider elevenlabs --audio ./sample.wav`.
308
+ `--audio` is repeatable for ElevenLabs; Qwen (`--provider labs`, the default)
309
+ requires one sample. Saving returns a voice card immediately. Use its ID with
310
+ `tts-create --model eleven_v3 --voice-id UUID --text "Hello"` or
311
+ `tts-clone --voice-id UUID --text "Hello"`. List voices with
312
+ `tts-voices --provider elevenlabs --offset 0 --limit 50`, and delete an owned
313
+ voice with `tts-delete --voice-id UUID`.
314
+
293
315
  ### Chat
294
316
 
295
317
  ```bash
@@ -369,6 +391,12 @@ Passing `--download` always waits for a successful result before saving files:
369
391
  everypixel wait TASK_ID --download ./outputs
370
392
  ```
371
393
 
394
+ `elapsed_sec` is total local waiting time; `queue_sec` estimates time until the
395
+ first observed `STARTED` response.
396
+
397
+ API costs are USD strings: `estimated_cost` for pending tasks and `billed_cost`
398
+ for successful results and chat. Missing costs are omitted; `"0"` means no charge.
399
+
372
400
  A Bash automation example:
373
401
 
374
402
  ```bash
@@ -229,9 +229,12 @@ They support 4K output. The default `seedvr2` supports up to 1440p, with a
229
229
  | `everypixel lipsync video` | Synchronize a video with an audio track |
230
230
  | `everypixel lipsync image` | Create a lipsync video from a still image and audio |
231
231
  | `everypixel audio transcribe` | Transcribe speech from an audio file |
232
- | `everypixel audio tts-create` | Create speech with a built-in speaker |
233
- | `everypixel audio tts-clone` | Create speech from a cloned voice sample |
234
- | `everypixel audio tts-voice` | Create speech with a character voice |
232
+ | `everypixel audio tts-create` | Create speech with a Qwen preset or saved ElevenLabs voice |
233
+ | `everypixel audio tts-clone` | Create Qwen speech from an audio sample or saved voice |
234
+ | `everypixel audio tts-design` | Design voice previews from a description |
235
+ | `everypixel audio tts-voice` | Save audio samples or a design preview as a voice |
236
+ | `everypixel audio tts-voices` | List presets and saved voices |
237
+ | `everypixel audio tts-delete` | Delete an owned saved voice |
235
238
 
236
239
  Lipsync supports 720p; the API checks source media dimensions and duration.
237
240
 
@@ -256,6 +259,25 @@ everypixel audio tts-create \
256
259
  --download ./outputs
257
260
  ```
258
261
 
262
+ `tts-create` lets the API choose the model when `--model` is omitted.
263
+ `tts-clone` and `tts-design` default to `--model qwen3`.
264
+ `--speaker`, `--style`, `--language`, and `--seed` are Qwen controls; `--character` applies only to Qwen voice design.
265
+ Qwen text is limited to 200 characters. ElevenLabs synthesis accepts up to
266
+ 2048 characters and requires `--voice-id`. Supported synthesis models are
267
+ `eleven_v4`, `eleven_v4_turbo`, `eleven_v3`, `eleven_v3_conversational`,
268
+ `eleven_multilingual_v2`, and `eleven_flash_v2_5`. ElevenLabs design uses
269
+ `eleven_multilingual_ttv_v2` or `eleven_ttv_v3`, with 100–1000 characters of
270
+ text and a 20–1000 character `--prompt`.
271
+
272
+ Save a returned preview with `tts-voice --name "Narrator" --preview-id UUID`, or save local
273
+ samples with `tts-voice --name "My voice" --provider elevenlabs --audio ./sample.wav`.
274
+ `--audio` is repeatable for ElevenLabs; Qwen (`--provider labs`, the default)
275
+ requires one sample. Saving returns a voice card immediately. Use its ID with
276
+ `tts-create --model eleven_v3 --voice-id UUID --text "Hello"` or
277
+ `tts-clone --voice-id UUID --text "Hello"`. List voices with
278
+ `tts-voices --provider elevenlabs --offset 0 --limit 50`, and delete an owned
279
+ voice with `tts-delete --voice-id UUID`.
280
+
259
281
  ### Chat
260
282
 
261
283
  ```bash
@@ -335,6 +357,12 @@ Passing `--download` always waits for a successful result before saving files:
335
357
  everypixel wait TASK_ID --download ./outputs
336
358
  ```
337
359
 
360
+ `elapsed_sec` is total local waiting time; `queue_sec` estimates time until the
361
+ first observed `STARTED` response.
362
+
363
+ API costs are USD strings: `estimated_cost` for pending tasks and `billed_cost`
364
+ for successful results and chat. Missing costs are omitted; `"0"` means no charge.
365
+
338
366
  A Bash automation example:
339
367
 
340
368
  ```bash
@@ -0,0 +1 @@
1
+ __version__ = "0.4.1"
@@ -14,6 +14,8 @@ def serialize_operation_result(result: OperationResult) -> Any:
14
14
  value = result.value
15
15
  if isinstance(value, Mapping):
16
16
  payload = dict(value)
17
+ if "task_id" in payload and payload.get("status") == "SUCCESS":
18
+ payload.pop("estimated_cost", None)
17
19
  if result.saved_files:
18
20
  payload["downloaded"] = [str(path) for path in result.saved_files]
19
21
  return payload
@@ -34,6 +34,13 @@ from ..openapi import (
34
34
  )
35
35
  from ..schemas import (
36
36
  ChatRequest,
37
+ TTS_CREATE_PAYLOADS,
38
+ TTS_DESIGN_PAYLOADS,
39
+ TTSClonePayload,
40
+ TTSDefaultCreatePayload,
41
+ TTSVoiceSavePayload,
42
+ TTSVoicesPayload,
43
+ TTSVoiceDeletePayload,
37
44
  GPT_IMAGE_MODEL_QUALITIES,
38
45
  ImageEditPayload,
39
46
  ImageGeneratePayload,
@@ -205,14 +212,20 @@ class TaskService:
205
212
 
206
213
  def wait(self, task_id: str, options: ExecutionOptions) -> dict[str, Any]:
207
214
  started = time.monotonic()
215
+ queue_sec: float | None = None
208
216
  while True:
209
217
  options.check_cancelled()
210
218
  payload = self.status(task_id)
211
219
  options.check_cancelled()
220
+ elapsed = time.monotonic() - started
221
+ if payload.get("status") == "STARTED" and queue_sec is None:
222
+ queue_sec = round(elapsed, 2)
212
223
  if payload.get("status") == "SUCCESS":
213
- payload["elapsed_sec"] = round(time.monotonic() - started, 2)
224
+ payload["elapsed_sec"] = round(elapsed, 2)
225
+ if queue_sec is not None:
226
+ payload["queue_sec"] = queue_sec
214
227
  return payload
215
- if time.monotonic() - started >= options.timeout:
228
+ if elapsed >= options.timeout:
216
229
  raise TaskPollingError(
217
230
  f"Timed out waiting for task {task_id}",
218
231
  code="timeout",
@@ -240,11 +253,14 @@ class OperationService:
240
253
  def execute(self, request: OperationRequest) -> OperationResult:
241
254
  method = request.method.upper()
242
255
  payload = dict(request.payload)
256
+ use_query = method == "GET" or (
257
+ method == "DELETE" and request.endpoint == "/v1/tts_voice"
258
+ )
243
259
  response = self.client.request(
244
260
  method,
245
261
  request.endpoint,
246
- json=payload if method != "GET" else None,
247
- params=payload if method == "GET" else None,
262
+ json=None if use_query else payload,
263
+ params=payload if use_query else None,
248
264
  )
249
265
  if not isinstance(response, Mapping) or "task_id" not in response:
250
266
  return OperationResult(value=response)
@@ -753,33 +769,64 @@ def read_text_input(text: str | None, text_file: Path | None) -> str:
753
769
  return text
754
770
 
755
771
 
756
- def build_tts_create_payload(**values: Any) -> dict[str, Any]:
757
- """Build a text-to-speech creation request."""
772
+ def validate_tts_payload(
773
+ path: str, values: dict[str, Any], *, generic: bool = False
774
+ ) -> BaseModel:
775
+ """Select a provider schema without dropping unsupported controls."""
776
+ if generic:
777
+ schemas: list[type[BaseModel]] = [
778
+ *TTS_CREATE_PAYLOADS.values(),
779
+ *TTS_DESIGN_PAYLOADS.values(),
780
+ TTSClonePayload,
781
+ TTSDefaultCreatePayload,
782
+ ]
783
+ known_fields = {field for schema in schemas for field in schema.model_fields}
784
+ values = {key: value for key, value in values.items() if key in known_fields}
785
+ if path == "/v1/tts_create" and "model" not in values:
786
+ return TTSDefaultCreatePayload.model_validate(values)
787
+ registries = {
788
+ "/v1/tts_create": TTS_CREATE_PAYLOADS,
789
+ "/v1/tts_design": TTS_DESIGN_PAYLOADS,
790
+ }
791
+ if path in registries:
792
+ model = values.get("model", "qwen3")
793
+ schema = registries[path].get(model) if isinstance(model, str) else None
794
+ if schema is None:
795
+ raise ValidationCLIError("Unsupported TTS model")
796
+ return schema.model_validate(values)
797
+ return TTSClonePayload.model_validate(values)
798
+
758
799
 
800
+ def _build_tts_payload(path: str, values: dict[str, Any]) -> dict[str, Any]:
759
801
  text = values.pop("text")
760
802
  text_file = values.pop("text_file")
761
- return {"text": read_text_input(text, text_file), **values}
803
+ values = {key: value for key, value in values.items() if value is not None}
804
+ values["text"] = read_text_input(text, text_file)
805
+ return validate_tts_payload(path, values).model_dump(mode="json", exclude_none=True)
762
806
 
763
807
 
764
- def build_tts_voice_payload(**values: Any) -> dict[str, Any]:
765
- """Build a character voice text-to-speech request."""
808
+ def build_tts_create_payload(**values: Any) -> dict[str, Any]:
809
+ return _build_tts_payload("/v1/tts_create", values)
766
810
 
767
- text = values.pop("text")
768
- text_file = values.pop("text_file")
769
- return {"text": read_text_input(text, text_file), **values}
811
+
812
+ def build_tts_design_payload(**values: Any) -> dict[str, Any]:
813
+ return _build_tts_payload("/v1/tts_design", values)
770
814
 
771
815
 
772
816
  def build_tts_clone_payload(**values: Any) -> dict[str, Any]:
773
- """Build a cloned-voice text-to-speech request."""
817
+ audio = values.pop("audio")
818
+ if audio is not None:
819
+ values["audio_url"] = media_value(audio)
820
+ return _build_tts_payload("/v1/tts_clone", values)
821
+
774
822
 
823
+ def build_tts_voice_payload(**values: Any) -> dict[str, Any]:
775
824
  audio = values.pop("audio")
776
- text = values.pop("text")
777
- text_file = values.pop("text_file")
778
- return {
779
- "audio_url": media_value(audio),
780
- "text": read_text_input(text, text_file),
781
- **values,
782
- }
825
+ if audio:
826
+ values["audio_urls"] = [media_value(sample) for sample in audio]
827
+ return TTSVoiceSavePayload.model_validate(
828
+ {key: value for key, value in values.items() if value is not None}
829
+ ).model_dump(mode="json", exclude_none=True)
783
830
 
784
831
 
785
832
  @dataclass
@@ -1147,25 +1194,28 @@ class ApplicationServices:
1147
1194
  *,
1148
1195
  text: str | None,
1149
1196
  text_file: Path | None,
1150
- speaker: str,
1151
- style: str,
1152
- language: str,
1153
- prompt: str,
1154
- seed: int,
1155
1197
  execution: ExecutionOptions,
1198
+ model: str | None = None,
1199
+ voice_id: str | None = None,
1200
+ speaker: str | None = None,
1201
+ style: str | None = None,
1202
+ language: str | None = None,
1203
+ prompt: str | None = None,
1204
+ seed: int | None = None,
1156
1205
  ) -> OperationResult:
1157
- payload = build_tts_create_payload(
1158
- text=text,
1159
- text_file=text_file,
1160
- speaker=speaker,
1161
- style=style,
1162
- language=language,
1163
- prompt=prompt,
1164
- seed=seed,
1165
- )
1166
1206
  return self._execute_async(
1167
1207
  endpoint="/v1/tts_create",
1168
- payload=payload,
1208
+ payload=build_tts_create_payload(
1209
+ text=text,
1210
+ text_file=text_file,
1211
+ model=model,
1212
+ voice_id=voice_id,
1213
+ speaker=speaker,
1214
+ style=style,
1215
+ language=language,
1216
+ prompt=prompt,
1217
+ seed=seed,
1218
+ ),
1169
1219
  execution=execution,
1170
1220
  fallback_extension=".mp3",
1171
1221
  )
@@ -1173,55 +1223,113 @@ class ApplicationServices:
1173
1223
  def execute_tts_clone(
1174
1224
  self,
1175
1225
  *,
1176
- audio: str,
1177
1226
  text: str | None,
1178
1227
  text_file: Path | None,
1179
- language: str,
1180
- seed: int,
1181
1228
  execution: ExecutionOptions,
1229
+ audio: str | None = None,
1230
+ voice_id: str | None = None,
1231
+ model: str = "qwen3",
1232
+ language: str | None = None,
1233
+ seed: int | None = None,
1182
1234
  ) -> OperationResult:
1183
- payload = build_tts_clone_payload(
1184
- audio=audio,
1185
- text=text,
1186
- text_file=text_file,
1187
- language=language,
1188
- seed=seed,
1189
- )
1190
1235
  return self._execute_async(
1191
1236
  endpoint="/v1/tts_clone",
1192
- payload=payload,
1237
+ payload=build_tts_clone_payload(
1238
+ audio=audio,
1239
+ text=text,
1240
+ text_file=text_file,
1241
+ voice_id=voice_id,
1242
+ model=model,
1243
+ language=language,
1244
+ seed=seed,
1245
+ ),
1193
1246
  execution=execution,
1194
1247
  fallback_extension=".mp3",
1195
1248
  )
1196
1249
 
1197
- def execute_tts_voice(
1250
+ def execute_tts_design(
1198
1251
  self,
1199
1252
  *,
1200
1253
  text: str | None,
1201
1254
  text_file: Path | None,
1202
- character: str,
1203
- style: str,
1204
- language: str,
1205
- prompt: str,
1206
- seed: int,
1207
1255
  execution: ExecutionOptions,
1256
+ model: str = "qwen3",
1257
+ character: str | None = None,
1258
+ style: str | None = None,
1259
+ language: str | None = None,
1260
+ prompt: str | None = None,
1261
+ seed: int | None = None,
1208
1262
  ) -> OperationResult:
1209
- payload = build_tts_voice_payload(
1210
- text=text,
1211
- text_file=text_file,
1212
- character=character,
1213
- style=style,
1214
- language=language,
1215
- prompt=prompt,
1216
- seed=seed,
1217
- )
1218
1263
  return self._execute_async(
1219
- endpoint="/v1/tts_voice",
1220
- payload=payload,
1264
+ endpoint="/v1/tts_design",
1265
+ payload=build_tts_design_payload(
1266
+ text=text,
1267
+ text_file=text_file,
1268
+ model=model,
1269
+ character=character,
1270
+ style=style,
1271
+ language=language,
1272
+ prompt=prompt,
1273
+ seed=seed,
1274
+ ),
1221
1275
  execution=execution,
1222
1276
  fallback_extension=".mp3",
1223
1277
  )
1224
1278
 
1279
+ def execute_tts_voice(
1280
+ self,
1281
+ *,
1282
+ name: str,
1283
+ audio: list[str],
1284
+ provider: str | None = None,
1285
+ preview_id: str | None = None,
1286
+ description: str = "",
1287
+ ) -> OperationResult:
1288
+ return self._operations.execute(
1289
+ OperationRequest(
1290
+ endpoint="/v1/tts_voice",
1291
+ method="POST",
1292
+ payload=build_tts_voice_payload(
1293
+ name=name,
1294
+ audio=audio,
1295
+ provider=provider,
1296
+ preview_id=preview_id,
1297
+ description=description,
1298
+ ),
1299
+ execution=ExecutionOptions(),
1300
+ )
1301
+ )
1302
+
1303
+ def list_tts_voices(
1304
+ self,
1305
+ *,
1306
+ provider: str = "labs",
1307
+ offset: int = 0,
1308
+ limit: int = 50,
1309
+ ) -> OperationResult:
1310
+ return self._operations.execute(
1311
+ OperationRequest(
1312
+ endpoint="/v1/tts_voices",
1313
+ method="GET",
1314
+ payload=TTSVoicesPayload.model_validate(
1315
+ {"provider": provider, "offset": offset, "limit": limit}
1316
+ ).model_dump(),
1317
+ execution=ExecutionOptions(),
1318
+ )
1319
+ )
1320
+
1321
+ def delete_tts_voice(self, *, voice_id: str) -> OperationResult:
1322
+ return self._operations.execute(
1323
+ OperationRequest(
1324
+ endpoint="/v1/tts_voice",
1325
+ method="DELETE",
1326
+ payload=TTSVoiceDeletePayload.model_validate(
1327
+ {"voice_id": voice_id}
1328
+ ).model_dump(mode="json"),
1329
+ execution=ExecutionOptions(),
1330
+ )
1331
+ )
1332
+
1225
1333
  def execute_chat(
1226
1334
  self,
1227
1335
  *,
@@ -1340,6 +1448,19 @@ class ApplicationServices:
1340
1448
  _VIDEO_EDIT_ADAPTER.validate_python(payload, extra="ignore")
1341
1449
  if path == "/v1/chat/completions":
1342
1450
  ChatRequest.model_validate(payload, extra="ignore")
1451
+ if resolved_method == "POST" and path in {
1452
+ "/v1/tts_create",
1453
+ "/v1/tts_clone",
1454
+ "/v1/tts_design",
1455
+ }:
1456
+ validate_tts_payload(path, payload, generic=True)
1457
+ if path == "/v1/tts_voice":
1458
+ if resolved_method == "POST":
1459
+ TTSVoiceSavePayload.model_validate(payload)
1460
+ elif resolved_method == "DELETE":
1461
+ TTSVoiceDeletePayload.model_validate(payload)
1462
+ if path == "/v1/tts_voices" and resolved_method == "GET":
1463
+ TTSVoicesPayload.model_validate(payload)
1343
1464
  validate_payload_against_operation(operation, payload)
1344
1465
  if dry_run:
1345
1466
  return OperationResult(
@@ -1285,17 +1285,19 @@ def tts_create(
1285
1285
  ctx: typer.Context,
1286
1286
  text: Annotated[Optional[str], typer.Option("--text")] = None,
1287
1287
  text_file: Annotated[Optional[Path], typer.Option("--text-file")] = None,
1288
- speaker: str = "Ryan",
1289
- style: str = "Auto",
1290
- language: str = "Auto",
1291
- prompt: str = "",
1292
- seed: int = -1,
1288
+ model: Optional[str] = None,
1289
+ voice_id: Optional[str] = None,
1290
+ speaker: Optional[str] = None,
1291
+ style: Optional[str] = None,
1292
+ language: Optional[str] = None,
1293
+ prompt: Optional[str] = None,
1294
+ seed: Optional[int] = None,
1293
1295
  wait_option: WaitOption = None,
1294
1296
  download: DownloadOption = None,
1295
1297
  output_json: OutputJsonOption = False,
1296
1298
  jq_expr: JqOption = None,
1297
1299
  ) -> None:
1298
- """Create speech from text using a selected speaker."""
1300
+ """Create speech using Qwen presets or a saved ElevenLabs voice."""
1299
1301
 
1300
1302
  runtime = apply_common_options(
1301
1303
  ctx,
@@ -1309,6 +1311,8 @@ def tts_create(
1309
1311
  lambda: runtime.services().execute_tts_create(
1310
1312
  text=text,
1311
1313
  text_file=text_file,
1314
+ model=model,
1315
+ voice_id=voice_id,
1312
1316
  speaker=speaker,
1313
1317
  style=style,
1314
1318
  language=language,
@@ -1322,11 +1326,13 @@ def tts_create(
1322
1326
  @audio_app.command("tts-clone")
1323
1327
  def tts_clone(
1324
1328
  ctx: typer.Context,
1325
- audio: Annotated[str, typer.Option("--audio")],
1329
+ audio: Annotated[Optional[str], typer.Option("--audio")] = None,
1330
+ voice_id: Optional[str] = None,
1326
1331
  text: Annotated[Optional[str], typer.Option("--text")] = None,
1327
1332
  text_file: Annotated[Optional[Path], typer.Option("--text-file")] = None,
1328
- language: str = "Auto",
1329
- seed: int = -1,
1333
+ model: str = "qwen3",
1334
+ language: Optional[str] = None,
1335
+ seed: Optional[int] = None,
1330
1336
  wait_option: WaitOption = None,
1331
1337
  download: DownloadOption = None,
1332
1338
  output_json: OutputJsonOption = False,
@@ -1345,8 +1351,10 @@ def tts_clone(
1345
1351
  ctx,
1346
1352
  lambda: runtime.services().execute_tts_clone(
1347
1353
  audio=audio,
1354
+ voice_id=voice_id,
1348
1355
  text=text,
1349
1356
  text_file=text_file,
1357
+ model=model,
1350
1358
  language=language,
1351
1359
  seed=seed,
1352
1360
  execution=runtime.execution_options(),
@@ -1354,22 +1362,23 @@ def tts_clone(
1354
1362
  )
1355
1363
 
1356
1364
 
1357
- @audio_app.command("tts-voice")
1358
- def tts_voice(
1365
+ @audio_app.command("tts-design")
1366
+ def tts_design(
1359
1367
  ctx: typer.Context,
1360
1368
  text: Annotated[Optional[str], typer.Option("--text")] = None,
1361
1369
  text_file: Annotated[Optional[Path], typer.Option("--text-file")] = None,
1362
- character: str = "Female",
1363
- style: str = "Auto",
1364
- language: str = "Auto",
1365
- prompt: str = "",
1366
- seed: int = -1,
1370
+ model: str = "qwen3",
1371
+ character: Optional[str] = None,
1372
+ style: Optional[str] = None,
1373
+ language: Optional[str] = None,
1374
+ prompt: Optional[str] = None,
1375
+ seed: Optional[int] = None,
1367
1376
  wait_option: WaitOption = None,
1368
1377
  download: DownloadOption = None,
1369
1378
  output_json: OutputJsonOption = False,
1370
1379
  jq_expr: JqOption = None,
1371
1380
  ) -> None:
1372
- """Create speech from text using a character voice."""
1381
+ """Design voice previews from a description."""
1373
1382
 
1374
1383
  runtime = apply_common_options(
1375
1384
  ctx,
@@ -1380,9 +1389,10 @@ def tts_voice(
1380
1389
  )
1381
1390
  run_action(
1382
1391
  ctx,
1383
- lambda: runtime.services().execute_tts_voice(
1392
+ lambda: runtime.services().execute_tts_design(
1384
1393
  text=text,
1385
1394
  text_file=text_file,
1395
+ model=model,
1386
1396
  character=character,
1387
1397
  style=style,
1388
1398
  language=language,
@@ -1393,6 +1403,64 @@ def tts_voice(
1393
1403
  )
1394
1404
 
1395
1405
 
1406
+ @audio_app.command("tts-voice")
1407
+ def tts_voice(
1408
+ ctx: typer.Context,
1409
+ name: Annotated[str, typer.Option("--name")],
1410
+ audio: Annotated[list[str], typer.Option("--audio")] = [],
1411
+ provider: Optional[str] = None,
1412
+ preview_id: Optional[str] = None,
1413
+ description: str = "",
1414
+ output_json: OutputJsonOption = False,
1415
+ jq_expr: JqOption = None,
1416
+ ) -> None:
1417
+ """Save a voice from audio samples or a design preview."""
1418
+ runtime = apply_common_options(ctx, output_json=output_json, jq_expr=jq_expr)
1419
+ run_action(
1420
+ ctx,
1421
+ lambda: runtime.services().execute_tts_voice(
1422
+ name=name,
1423
+ audio=audio,
1424
+ provider=provider,
1425
+ preview_id=preview_id,
1426
+ description=description,
1427
+ ),
1428
+ )
1429
+
1430
+
1431
+ @audio_app.command("tts-voices")
1432
+ def tts_voices(
1433
+ ctx: typer.Context,
1434
+ provider: str = "labs",
1435
+ offset: int = 0,
1436
+ limit: int = 50,
1437
+ output_json: OutputJsonOption = False,
1438
+ jq_expr: JqOption = None,
1439
+ ) -> None:
1440
+ """List presets and saved voices for a provider."""
1441
+ runtime = apply_common_options(ctx, output_json=output_json, jq_expr=jq_expr)
1442
+ run_action(
1443
+ ctx,
1444
+ lambda: runtime.services().list_tts_voices(
1445
+ provider=provider,
1446
+ offset=offset,
1447
+ limit=limit,
1448
+ ),
1449
+ )
1450
+
1451
+
1452
+ @audio_app.command("tts-delete")
1453
+ def tts_delete(
1454
+ ctx: typer.Context,
1455
+ voice_id: Annotated[str, typer.Option("--voice-id")],
1456
+ output_json: OutputJsonOption = False,
1457
+ jq_expr: JqOption = None,
1458
+ ) -> None:
1459
+ """Delete an owned saved voice."""
1460
+ runtime = apply_common_options(ctx, output_json=output_json, jq_expr=jq_expr)
1461
+ run_action(ctx, lambda: runtime.services().delete_tts_voice(voice_id=voice_id))
1462
+
1463
+
1396
1464
  @app.command("status")
1397
1465
  def status(
1398
1466
  ctx: typer.Context,