everypixel-cli 0.3.0__tar.gz → 0.4.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 (25) hide show
  1. {everypixel_cli-0.3.0 → everypixel_cli-0.4.0}/CHANGELOG.md +5 -0
  2. {everypixel_cli-0.3.0 → everypixel_cli-0.4.0}/PKG-INFO +26 -4
  3. {everypixel_cli-0.3.0 → everypixel_cli-0.4.0}/README.md +25 -3
  4. everypixel_cli-0.4.0/src/everypixel_cli/__init__.py +1 -0
  5. {everypixel_cli-0.3.0 → everypixel_cli-0.4.0}/src/everypixel_cli/application/services.py +176 -61
  6. {everypixel_cli-0.3.0 → everypixel_cli-0.4.0}/src/everypixel_cli/cli.py +86 -18
  7. {everypixel_cli-0.3.0 → everypixel_cli-0.4.0}/src/everypixel_cli/mcp_server.py +65 -19
  8. {everypixel_cli-0.3.0 → everypixel_cli-0.4.0}/src/everypixel_cli/resources/openapi.json +370 -40
  9. {everypixel_cli-0.3.0 → everypixel_cli-0.4.0}/src/everypixel_cli/schemas.py +285 -1
  10. everypixel_cli-0.3.0/src/everypixel_cli/__init__.py +0 -1
  11. {everypixel_cli-0.3.0 → everypixel_cli-0.4.0}/.gitignore +0 -0
  12. {everypixel_cli-0.3.0 → everypixel_cli-0.4.0}/LICENSE +0 -0
  13. {everypixel_cli-0.3.0 → everypixel_cli-0.4.0}/pyproject.toml +0 -0
  14. {everypixel_cli-0.3.0 → everypixel_cli-0.4.0}/src/everypixel_cli/__main__.py +0 -0
  15. {everypixel_cli-0.3.0 → everypixel_cli-0.4.0}/src/everypixel_cli/application/__init__.py +0 -0
  16. {everypixel_cli-0.3.0 → everypixel_cli-0.4.0}/src/everypixel_cli/application/chat.py +0 -0
  17. {everypixel_cli-0.3.0 → everypixel_cli-0.4.0}/src/everypixel_cli/application/models.py +0 -0
  18. {everypixel_cli-0.3.0 → everypixel_cli-0.4.0}/src/everypixel_cli/application/serialization.py +0 -0
  19. {everypixel_cli-0.3.0 → everypixel_cli-0.4.0}/src/everypixel_cli/client.py +0 -0
  20. {everypixel_cli-0.3.0 → everypixel_cli-0.4.0}/src/everypixel_cli/config.py +0 -0
  21. {everypixel_cli-0.3.0 → everypixel_cli-0.4.0}/src/everypixel_cli/errors.py +0 -0
  22. {everypixel_cli-0.3.0 → everypixel_cli-0.4.0}/src/everypixel_cli/files.py +0 -0
  23. {everypixel_cli-0.3.0 → everypixel_cli-0.4.0}/src/everypixel_cli/openapi.py +0 -0
  24. {everypixel_cli-0.3.0 → everypixel_cli-0.4.0}/src/everypixel_cli/output.py +0 -0
  25. {everypixel_cli-0.3.0 → everypixel_cli-0.4.0}/src/everypixel_cli/resources/__init__.py +0 -0
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.0] - 2026-10-07
4
+
5
+ - Add ElevenLabs speech synthesis and voice design.
6
+ - Add saved voice creation, listing, deletion, and Qwen cloning by voice ID.
7
+
3
8
  ## [0.3.0] - 2026-09-18
4
9
 
5
10
  - 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.0
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
@@ -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
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0"
@@ -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,
@@ -240,11 +247,14 @@ class OperationService:
240
247
  def execute(self, request: OperationRequest) -> OperationResult:
241
248
  method = request.method.upper()
242
249
  payload = dict(request.payload)
250
+ use_query = method == "GET" or (
251
+ method == "DELETE" and request.endpoint == "/v1/tts_voice"
252
+ )
243
253
  response = self.client.request(
244
254
  method,
245
255
  request.endpoint,
246
- json=payload if method != "GET" else None,
247
- params=payload if method == "GET" else None,
256
+ json=None if use_query else payload,
257
+ params=payload if use_query else None,
248
258
  )
249
259
  if not isinstance(response, Mapping) or "task_id" not in response:
250
260
  return OperationResult(value=response)
@@ -753,33 +763,64 @@ def read_text_input(text: str | None, text_file: Path | None) -> str:
753
763
  return text
754
764
 
755
765
 
756
- def build_tts_create_payload(**values: Any) -> dict[str, Any]:
757
- """Build a text-to-speech creation request."""
766
+ def validate_tts_payload(
767
+ path: str, values: dict[str, Any], *, generic: bool = False
768
+ ) -> BaseModel:
769
+ """Select a provider schema without dropping unsupported controls."""
770
+ if generic:
771
+ schemas: list[type[BaseModel]] = [
772
+ *TTS_CREATE_PAYLOADS.values(),
773
+ *TTS_DESIGN_PAYLOADS.values(),
774
+ TTSClonePayload,
775
+ TTSDefaultCreatePayload,
776
+ ]
777
+ known_fields = {field for schema in schemas for field in schema.model_fields}
778
+ values = {key: value for key, value in values.items() if key in known_fields}
779
+ if path == "/v1/tts_create" and "model" not in values:
780
+ return TTSDefaultCreatePayload.model_validate(values)
781
+ registries = {
782
+ "/v1/tts_create": TTS_CREATE_PAYLOADS,
783
+ "/v1/tts_design": TTS_DESIGN_PAYLOADS,
784
+ }
785
+ if path in registries:
786
+ model = values.get("model", "qwen3")
787
+ schema = registries[path].get(model) if isinstance(model, str) else None
788
+ if schema is None:
789
+ raise ValidationCLIError("Unsupported TTS model")
790
+ return schema.model_validate(values)
791
+ return TTSClonePayload.model_validate(values)
792
+
758
793
 
794
+ def _build_tts_payload(path: str, values: dict[str, Any]) -> dict[str, Any]:
759
795
  text = values.pop("text")
760
796
  text_file = values.pop("text_file")
761
- return {"text": read_text_input(text, text_file), **values}
797
+ values = {key: value for key, value in values.items() if value is not None}
798
+ values["text"] = read_text_input(text, text_file)
799
+ return validate_tts_payload(path, values).model_dump(mode="json", exclude_none=True)
762
800
 
763
801
 
764
- def build_tts_voice_payload(**values: Any) -> dict[str, Any]:
765
- """Build a character voice text-to-speech request."""
802
+ def build_tts_create_payload(**values: Any) -> dict[str, Any]:
803
+ return _build_tts_payload("/v1/tts_create", values)
766
804
 
767
- text = values.pop("text")
768
- text_file = values.pop("text_file")
769
- return {"text": read_text_input(text, text_file), **values}
805
+
806
+ def build_tts_design_payload(**values: Any) -> dict[str, Any]:
807
+ return _build_tts_payload("/v1/tts_design", values)
770
808
 
771
809
 
772
810
  def build_tts_clone_payload(**values: Any) -> dict[str, Any]:
773
- """Build a cloned-voice text-to-speech request."""
811
+ audio = values.pop("audio")
812
+ if audio is not None:
813
+ values["audio_url"] = media_value(audio)
814
+ return _build_tts_payload("/v1/tts_clone", values)
815
+
774
816
 
817
+ def build_tts_voice_payload(**values: Any) -> dict[str, Any]:
775
818
  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
- }
819
+ if audio:
820
+ values["audio_urls"] = [media_value(sample) for sample in audio]
821
+ return TTSVoiceSavePayload.model_validate(
822
+ {key: value for key, value in values.items() if value is not None}
823
+ ).model_dump(mode="json", exclude_none=True)
783
824
 
784
825
 
785
826
  @dataclass
@@ -1147,25 +1188,28 @@ class ApplicationServices:
1147
1188
  *,
1148
1189
  text: str | None,
1149
1190
  text_file: Path | None,
1150
- speaker: str,
1151
- style: str,
1152
- language: str,
1153
- prompt: str,
1154
- seed: int,
1155
1191
  execution: ExecutionOptions,
1192
+ model: str | None = None,
1193
+ voice_id: str | None = None,
1194
+ speaker: str | None = None,
1195
+ style: str | None = None,
1196
+ language: str | None = None,
1197
+ prompt: str | None = None,
1198
+ seed: int | None = None,
1156
1199
  ) -> 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
1200
  return self._execute_async(
1167
1201
  endpoint="/v1/tts_create",
1168
- payload=payload,
1202
+ payload=build_tts_create_payload(
1203
+ text=text,
1204
+ text_file=text_file,
1205
+ model=model,
1206
+ voice_id=voice_id,
1207
+ speaker=speaker,
1208
+ style=style,
1209
+ language=language,
1210
+ prompt=prompt,
1211
+ seed=seed,
1212
+ ),
1169
1213
  execution=execution,
1170
1214
  fallback_extension=".mp3",
1171
1215
  )
@@ -1173,55 +1217,113 @@ class ApplicationServices:
1173
1217
  def execute_tts_clone(
1174
1218
  self,
1175
1219
  *,
1176
- audio: str,
1177
1220
  text: str | None,
1178
1221
  text_file: Path | None,
1179
- language: str,
1180
- seed: int,
1181
1222
  execution: ExecutionOptions,
1223
+ audio: str | None = None,
1224
+ voice_id: str | None = None,
1225
+ model: str = "qwen3",
1226
+ language: str | None = None,
1227
+ seed: int | None = None,
1182
1228
  ) -> 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
1229
  return self._execute_async(
1191
1230
  endpoint="/v1/tts_clone",
1192
- payload=payload,
1231
+ payload=build_tts_clone_payload(
1232
+ audio=audio,
1233
+ text=text,
1234
+ text_file=text_file,
1235
+ voice_id=voice_id,
1236
+ model=model,
1237
+ language=language,
1238
+ seed=seed,
1239
+ ),
1193
1240
  execution=execution,
1194
1241
  fallback_extension=".mp3",
1195
1242
  )
1196
1243
 
1197
- def execute_tts_voice(
1244
+ def execute_tts_design(
1198
1245
  self,
1199
1246
  *,
1200
1247
  text: str | None,
1201
1248
  text_file: Path | None,
1202
- character: str,
1203
- style: str,
1204
- language: str,
1205
- prompt: str,
1206
- seed: int,
1207
1249
  execution: ExecutionOptions,
1250
+ model: str = "qwen3",
1251
+ character: str | None = None,
1252
+ style: str | None = None,
1253
+ language: str | None = None,
1254
+ prompt: str | None = None,
1255
+ seed: int | None = None,
1208
1256
  ) -> 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
1257
  return self._execute_async(
1219
- endpoint="/v1/tts_voice",
1220
- payload=payload,
1258
+ endpoint="/v1/tts_design",
1259
+ payload=build_tts_design_payload(
1260
+ text=text,
1261
+ text_file=text_file,
1262
+ model=model,
1263
+ character=character,
1264
+ style=style,
1265
+ language=language,
1266
+ prompt=prompt,
1267
+ seed=seed,
1268
+ ),
1221
1269
  execution=execution,
1222
1270
  fallback_extension=".mp3",
1223
1271
  )
1224
1272
 
1273
+ def execute_tts_voice(
1274
+ self,
1275
+ *,
1276
+ name: str,
1277
+ audio: list[str],
1278
+ provider: str | None = None,
1279
+ preview_id: str | None = None,
1280
+ description: str = "",
1281
+ ) -> OperationResult:
1282
+ return self._operations.execute(
1283
+ OperationRequest(
1284
+ endpoint="/v1/tts_voice",
1285
+ method="POST",
1286
+ payload=build_tts_voice_payload(
1287
+ name=name,
1288
+ audio=audio,
1289
+ provider=provider,
1290
+ preview_id=preview_id,
1291
+ description=description,
1292
+ ),
1293
+ execution=ExecutionOptions(),
1294
+ )
1295
+ )
1296
+
1297
+ def list_tts_voices(
1298
+ self,
1299
+ *,
1300
+ provider: str = "labs",
1301
+ offset: int = 0,
1302
+ limit: int = 50,
1303
+ ) -> OperationResult:
1304
+ return self._operations.execute(
1305
+ OperationRequest(
1306
+ endpoint="/v1/tts_voices",
1307
+ method="GET",
1308
+ payload=TTSVoicesPayload.model_validate(
1309
+ {"provider": provider, "offset": offset, "limit": limit}
1310
+ ).model_dump(),
1311
+ execution=ExecutionOptions(),
1312
+ )
1313
+ )
1314
+
1315
+ def delete_tts_voice(self, *, voice_id: str) -> OperationResult:
1316
+ return self._operations.execute(
1317
+ OperationRequest(
1318
+ endpoint="/v1/tts_voice",
1319
+ method="DELETE",
1320
+ payload=TTSVoiceDeletePayload.model_validate(
1321
+ {"voice_id": voice_id}
1322
+ ).model_dump(mode="json"),
1323
+ execution=ExecutionOptions(),
1324
+ )
1325
+ )
1326
+
1225
1327
  def execute_chat(
1226
1328
  self,
1227
1329
  *,
@@ -1340,6 +1442,19 @@ class ApplicationServices:
1340
1442
  _VIDEO_EDIT_ADAPTER.validate_python(payload, extra="ignore")
1341
1443
  if path == "/v1/chat/completions":
1342
1444
  ChatRequest.model_validate(payload, extra="ignore")
1445
+ if resolved_method == "POST" and path in {
1446
+ "/v1/tts_create",
1447
+ "/v1/tts_clone",
1448
+ "/v1/tts_design",
1449
+ }:
1450
+ validate_tts_payload(path, payload, generic=True)
1451
+ if path == "/v1/tts_voice":
1452
+ if resolved_method == "POST":
1453
+ TTSVoiceSavePayload.model_validate(payload)
1454
+ elif resolved_method == "DELETE":
1455
+ TTSVoiceDeletePayload.model_validate(payload)
1456
+ if path == "/v1/tts_voices" and resolved_method == "GET":
1457
+ TTSVoicesPayload.model_validate(payload)
1343
1458
  validate_payload_against_operation(operation, payload)
1344
1459
  if dry_run:
1345
1460
  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,