feat: US-006 - Refactor tools/tts.py — rutare engine, backend pocket-tts și fallback
- synthesize() rezolvă engine din tts_voices.json (fallback la tts.default_engine din config) - _synthesize_pockettts nou: POST multipart la :7789/tts cu voice_wav/voice_url, ignoră lang - fallback tehnic automat pe _synthesize_supertonic(M2, ro) via _PocketTTSUnavailable; erori de conținut (voce lipsă, text gol) nu declanșează fallback; rezultatul include engine_used - gates rulate: tests PASS (1043 passed, 22 eșecuri preexistente neschimbate), /workflow:simplify (4 agenți) cu 1 fix aplicat, /review (backend, manual) PASS
This commit is contained in:
@@ -159,12 +159,12 @@
|
|||||||
],
|
],
|
||||||
"requiresBrowserCheck": false,
|
"requiresBrowserCheck": false,
|
||||||
"requiresDesignReview": false,
|
"requiresDesignReview": false,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"failed": false,
|
"failed": false,
|
||||||
"blocked": false,
|
"blocked": false,
|
||||||
"retries": 0,
|
"retries": 1,
|
||||||
"failureReason": "",
|
"failureReason": "",
|
||||||
"notes": ""
|
"notes": "Fișiere: tools/tts.py (refactor). synthesize() rezolvă engine din tts_voices.json (entry.get(\"engine\") sau fallback la config tts.default_engine); _synthesize_supertonic (fost synthesize, cap _MAX_TTS_CHARS=400 + retry recursiv lang=\"na\" păstrate neschimbate) și _synthesize_pockettts (POST multipart la {pockettts_url}/tts cu voice_wav din state_path sau voice_url, ignoră lang) nou. _PocketTTSUnavailable (ConnectError/Timeout/5xx) declanșează fallback automat pe _synthesize_supertonic(voice=M2, lang=ro) conform AC; erori de conținut (fișier voce lipsă, 4xx, text gol) NU declanșează fallback. Rezultatul include engine_used. Semnătura publică synthesize(text, voice, lang) neschimbată — verificat caller src/fast_commands.py. Gate /workflow:simplify (refactor, 4 agenți reuse/simplification/efficiency/altitude): aplicat 1 fix sigur (engine_used setat o singură dată, nu triplu literal); respinse ca false-positive 3 findings (voice=M2/lang=ro hardcodat pe fallback e cerință explicită din AC, nu bandaid; lookup catalog case-insensitive ar schimba comportament fără beneficiu clar în scope); notat pentru follow-up neaplicat: caching catalog/config/voice-file per sesiune în hot path-ul de streaming (src/voice/tts_stream.py) — necesită schimbări în afara scope-ului acestui story. Gate /review (backend, manual): PASS — fără shell injection, fără secret leakage, error handling scoped corect (ConnectError/TimeoutException/HTTPStatusError), API contract păstrat. pytest tests/: aceleași 22 eșecuri preexistente, 1043 passed (neschimbat)."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "US-007",
|
"id": "US-007",
|
||||||
|
|||||||
@@ -44,3 +44,84 @@ Project: echo-core
|
|||||||
- US-003 (db+backend, catalog tts_voices.json) — eligibil, dependsOn gol
|
- US-003 (db+backend, catalog tts_voices.json) — eligibil, dependsOn gol
|
||||||
- US-005 (backend, config.json tts section) — eligibil, dependsOn gol
|
- US-005 (backend, config.json tts section) — eligibil, dependsOn gol
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Iterație: 2026-07-11 (US-004)
|
||||||
|
### Story implementat: US-004 - Script de export voce pocket_tts_add_voice.py (tags: backend)
|
||||||
|
### Status: Complete
|
||||||
|
|
||||||
|
### Gates rulate:
|
||||||
|
- Typecheck: SKIP (mypy neinstalat în .venv)
|
||||||
|
- Lint: SKIP (ruff neinstalat în .venv)
|
||||||
|
- Tests: PASS — pytest tests/ aceleași 22 eșecuri preexistente, 1043 passed (neschimbat)
|
||||||
|
- /review (backend): PASS — script mic, single-responsibility, erori clare (wav lipsă / hf_token lipsă din keyring), fără shell injection / secret leakage (HF_TOKEN doar în os.environ al subprocesului, niciodată logat)
|
||||||
|
- Verificare manuală: .venv-pockettts/bin/python tools/pocket_tts_add_voice.py --help rulează curat fără a atinge .venv principal; _next_free_index() -> 4 pentru Marius/Paula (seed 1-3 deja ocupat), 1 pentru nume noi; toate cele 6 blob-uri models/voices/{marius,paula}-{1,2,3}.safetensors validate cu safetensors.torch.load_file (12 tensori fiecare, fără corupere)
|
||||||
|
|
||||||
|
### Learnings:
|
||||||
|
- Iterația anterioară (US-004, retries=1) a atins max_turns=30 în timp ce lucra pe US-003 și US-004 în aceeași sesiune (script scris, model încărcat, 6 voci exportate, catalog scris) dar nu a apucat să facă commit — la începutul iterației verifică mereu `git status`/`git log` înainte să presupui că trebuie reimplementat de la zero; codul + blob-urile deja produse pot fi validate în loc de regenerate (re-exportul ar fi reîncărcat modelul, costisitor).
|
||||||
|
- `tools/pocket_tts_add_voice.py` a ajuns commitat accidental în același commit cu US-003 (e3b5cdf) pentru că era deja `git add`-uit din sesiunea anterioară — commit-urile per-story nu sunt garantat izolate dacă lucrul anterior a lăsat fișiere staged; verifică `git diff --cached` înainte de commit ca să știi exact ce intră.
|
||||||
|
- `scripts/ralph/prd.json` (harness PRD-ul acestui proiect) și `cron/jobs.json` (timestamp-uri live ale schedulerului) nu trebuie commitate ca parte a unui story de cod — doar prd.json e commitat, dar separat, ca update de stare, nu amestecat în diff-ul de cod al story-ului.
|
||||||
|
|
||||||
|
### Next:
|
||||||
|
- US-005 (backend, config.json secțiune tts) — eligibil, dependsOn gol
|
||||||
|
- US-010 (backend, /voice doctor + pocket-tts health) — eligibil, dependsOn US-001+US-002 (ambele passes:true)
|
||||||
|
---
|
||||||
|
|
||||||
|
## Iterație: 2026-07-11 (US-005)
|
||||||
|
### Story implementat: US-005 - Adaugă secțiunea tts în config.json (tags: backend)
|
||||||
|
### Status: Complete
|
||||||
|
|
||||||
|
### Gates rulate:
|
||||||
|
- Typecheck: SKIP (mypy neinstalat în .venv)
|
||||||
|
- Lint: SKIP (ruff neinstalat în .venv)
|
||||||
|
- Tests: PASS — pytest tests/ aceleași 22 eșecuri preexistente, 1043 passed (neschimbat)
|
||||||
|
- /review (backend): review manual (skill-ul interactiv /review nu se pretează unei iterații autonome — cere AskUserQuestion) — diff e doar date (4 linii JSON), fără cod nou care citește tts.* (US-006 face asta), fără secrete, JSON valid
|
||||||
|
|
||||||
|
### Learnings:
|
||||||
|
- src/config.py::Config e deja complet generic (get/set/save/reload cu dot-notation pe orice cheie nouă) — adăugarea unei secțiuni noi în config.json nu cere nicio schimbare de cod, doar date. tests/test_config.py deja acoperă roundtrip-ul set→save→reload generic, deci AC-urile de persistență ale story-ului sunt satisfăcute fără teste noi.
|
||||||
|
- Skill-ul gstack `/review` e construit pentru revizuire PR interactivă (AskUserQuestion, gh pr, telemetry) — pentru iterații Ralph autonome cu diff-uri mici/triviale, un review manual (grep pentru cod care citește noua cheie, verificare JSON valid, verificare secrete) e suficient și evită blocarea pe input interactiv indisponibil.
|
||||||
|
|
||||||
|
### Next:
|
||||||
|
- US-010 (backend, /voice doctor + pocket-tts health) — eligibil, dependsOn US-001+US-002 (ambele passes:true)
|
||||||
|
- US-006 (backend+refactor, rutare engine tts.py) — NU eligibil încă, dependsOn include US-003 (passes:true) dar și US-005 (acum passes:true) — devine eligibil următoarea iterație
|
||||||
|
---
|
||||||
|
|
||||||
|
## Iterație: 2026-07-11 (US-006)
|
||||||
|
### Story implementat: US-006 - Refactor tools/tts.py — rutare engine, backend pocket-tts și fallback (tags: backend, refactor)
|
||||||
|
### Status: Complete
|
||||||
|
|
||||||
|
### Gates rulate:
|
||||||
|
- Typecheck: SKIP (mypy neinstalat în .venv)
|
||||||
|
- Lint: SKIP (ruff neinstalat în .venv)
|
||||||
|
- Tests: PASS — pytest tests/ aceleași 22 eșecuri preexistente, 1043 passed (neschimbat)
|
||||||
|
- /workflow:simplify (refactor): 4 agenți paraleli (reuse/simplification/efficiency/altitude) pe diff-ul tools/tts.py.
|
||||||
|
Aplicat 1 fix sigur: `engine_used` setat o singură dată în loc de trei literali duplicate.
|
||||||
|
Respinse ca false-positive (verificate contra AC-urilor explicite din PRD, nu doar "pare hardcodat"):
|
||||||
|
- voice=DEFAULT_VOICE, lang=DEFAULT_LANG hardcodate pe calea de fallback tehnic — AC-ul US-006 cere explicit
|
||||||
|
`_synthesize_supertonic(voice='M2', lang='ro')`; pocket-tts ignoră lang complet, deci la fallback nu există
|
||||||
|
niciun semnal de încredere pentru ce limbă Supertonic ar trebui folosită — design intenționat, nu bandaid.
|
||||||
|
- lookup catalog case-insensitive unificat — ar schimba comportamentul (matching pe nume gen "Marius 1")
|
||||||
|
fără cerință din AC, risc fără beneficiu clar în scope-ul acestui story.
|
||||||
|
Notat pentru follow-up (NU implementat, în afara scope-ului): `_load_voice_catalog()`/`_config_get()`/citirea
|
||||||
|
fișierului de voce clonată se repetă per-clauză în hot path-ul de streaming (src/voice/tts_stream.py) — un
|
||||||
|
cache per-sesiune ar elimina I/O redundant, dar necesită schimbări în afara tools/tts.py.
|
||||||
|
- /review (backend, manual — skill-ul interactiv nu se pretează unei iterații autonome): PASS — fără shell
|
||||||
|
injection, fără secret leakage, error handling scoped corect (ConnectError/TimeoutException/HTTPStatusError
|
||||||
|
separate de erorile de conținut 4xx), semnătura publică synthesize(text, voice, lang) neschimbată (verificat
|
||||||
|
caller-ul src/fast_commands.py::_tts_synthesize).
|
||||||
|
|
||||||
|
### Learnings:
|
||||||
|
- Codul pentru US-006 era deja scris (necommitat) dintr-o iterație anterioară care nu a apucat să facă commit —
|
||||||
|
la fel ca US-003/US-004, verifică mereu `git diff`/`git status` înainte să presupui reimplementare de la zero.
|
||||||
|
- Gate-urile `/simplify` (agenți automați) găsesc uneori "probleme" care sunt de fapt cerințe explicite din AC
|
||||||
|
(ex. valorile hardcodate de fallback voice='M2'/lang='ro') — verifică fiecare finding contra textului literal
|
||||||
|
al acceptance criteria din PRD înainte să aplici un fix, nu doar contra intuiției generale de cod curat.
|
||||||
|
- Findings de eficiență care ar necesita schimbări cross-file (aici: caching în src/voice/tts_stream.py pentru
|
||||||
|
hot path-ul de streaming) rămân notate ca follow-up, nu aplicate — story-ul e scopat strict la tools/tts.py.
|
||||||
|
|
||||||
|
### Next:
|
||||||
|
- US-010 (backend, /voice doctor + pocket-tts health) — eligibil, dependsOn US-001+US-002 (ambele passes:true)
|
||||||
|
- US-007 (ui+backend, /voice engine) — eligibil, dependsOn US-005+US-006 (ambele acum passes:true)
|
||||||
|
- US-008 (ui+backend, /voice addvoice) — eligibil, dependsOn US-004+US-006 (ambele acum passes:true)
|
||||||
|
- US-009 (ui+backend, autocomplete voci) — eligibil, dependsOn US-003+US-006 (ambele acum passes:true)
|
||||||
|
- US-011 (backend, teste TTS) — eligibil, dependsOn US-006 (acum passes:true)
|
||||||
|
---
|
||||||
|
|||||||
134
tools/tts.py
134
tools/tts.py
@@ -1,9 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Text-to-speech via Supertonic local server.
|
"""Text-to-speech — rutează pe engine (Supertonic sau pocket-tts) pe baza catalogului de voci.
|
||||||
|
|
||||||
CLI:
|
CLI:
|
||||||
python3 tools/tts.py --text "Salut Marius" [--voice M1] [--lang ro]
|
python3 tools/tts.py --text "Salut Marius" [--voice M1] [--lang ro]
|
||||||
→ stdout: {"ok": true, "path": "/tmp/echo-tts-xxx.wav", "size_bytes": 12345}
|
→ stdout: {"ok": true, "path": "/tmp/echo-tts-xxx.wav", "size_bytes": 12345, "engine_used": "supertonic"}
|
||||||
→ stdout: {"ok": false, "error": "..."}
|
→ stdout: {"ok": false, "error": "..."}
|
||||||
|
|
||||||
Module:
|
Module:
|
||||||
@@ -15,13 +15,21 @@ import argparse
|
|||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
if str(REPO_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(REPO_ROOT))
|
||||||
|
|
||||||
SUPERTONIC_URL = "http://127.0.0.1:7788"
|
SUPERTONIC_URL = "http://127.0.0.1:7788"
|
||||||
VOICES = {"M1", "M2", "M3", "M4", "M5", "F1", "F2", "F3", "F4", "F5"}
|
VOICES = {"M1", "M2", "M3", "M4", "M5", "F1", "F2", "F3", "F4", "F5"}
|
||||||
DEFAULT_VOICE = "M2"
|
DEFAULT_VOICE = "M2"
|
||||||
DEFAULT_LANG = "ro"
|
DEFAULT_LANG = "ro"
|
||||||
|
DEFAULT_ENGINE = "supertonic"
|
||||||
|
|
||||||
|
_VOICES_CATALOG_PATH = REPO_ROOT / "tts_voices.json"
|
||||||
|
|
||||||
# Punctuation Supertonic synthesis rejects with HTTP 500 (Romanian curly quotes,
|
# Punctuation Supertonic synthesis rejects with HTTP 500 (Romanian curly quotes,
|
||||||
# smart dashes, ellipsis, angle quotes). Mapped to ASCII so a stray „foo" in
|
# smart dashes, ellipsis, angle quotes). Mapped to ASCII so a stray „foo" in
|
||||||
@@ -40,6 +48,10 @@ _TTS_PUNCT_MAP = {
|
|||||||
_MAX_TTS_CHARS = 400
|
_MAX_TTS_CHARS = 400
|
||||||
|
|
||||||
|
|
||||||
|
class _PocketTTSUnavailable(Exception):
|
||||||
|
"""pocket-tts server e nereachable sau eșuează server-side (5xx) — caller-ul trebuie să facă fallback pe Supertonic."""
|
||||||
|
|
||||||
|
|
||||||
def sanitize_for_supertonic(text: str) -> str:
|
def sanitize_for_supertonic(text: str) -> str:
|
||||||
"""Replace Unicode punctuation and strip chars that crash Supertonic's ONNX model."""
|
"""Replace Unicode punctuation and strip chars that crash Supertonic's ONNX model."""
|
||||||
for src, dst in _TTS_PUNCT_MAP.items():
|
for src, dst in _TTS_PUNCT_MAP.items():
|
||||||
@@ -58,16 +70,43 @@ def sanitize_for_supertonic(text: str) -> str:
|
|||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
def synthesize(text: str, voice: str = DEFAULT_VOICE, lang: str = DEFAULT_LANG) -> dict:
|
def _load_voice_catalog() -> dict:
|
||||||
|
from src.jsonlock import read_locked
|
||||||
|
|
||||||
|
try:
|
||||||
|
return read_locked(str(_VOICES_CATALOG_PATH))
|
||||||
|
except FileNotFoundError:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _catalog_entry(voice: str) -> dict | None:
|
||||||
|
catalog = _load_voice_catalog()
|
||||||
|
return catalog.get(voice) or catalog.get(voice.upper())
|
||||||
|
|
||||||
|
|
||||||
|
def _config_get(key: str, default: str) -> str:
|
||||||
|
from src.config import Config
|
||||||
|
|
||||||
|
return Config().get(key, default)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_temp_wav(content: bytes) -> dict:
|
||||||
|
try:
|
||||||
|
fd, path = tempfile.mkstemp(prefix="echo-tts-", suffix=".wav")
|
||||||
|
with open(fd, "wb") as f:
|
||||||
|
f.write(content)
|
||||||
|
return {"ok": True, "path": path, "size_bytes": len(content)}
|
||||||
|
except Exception as e:
|
||||||
|
return {"ok": False, "error": f"Scriere fișier: {e}"}
|
||||||
|
|
||||||
|
|
||||||
|
def _synthesize_supertonic(text: str, voice: str = DEFAULT_VOICE, lang: str = DEFAULT_LANG) -> dict:
|
||||||
"""Call Supertonic server and save audio to a temp WAV file.
|
"""Call Supertonic server and save audio to a temp WAV file.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
{"ok": True, "path": "/tmp/echo-tts-xxx.wav", "size_bytes": N}
|
{"ok": True, "path": "/tmp/echo-tts-xxx.wav", "size_bytes": N}
|
||||||
{"ok": False, "error": "mesaj eroare"}
|
{"ok": False, "error": "mesaj eroare"}
|
||||||
"""
|
"""
|
||||||
if not text or not text.strip():
|
|
||||||
return {"ok": False, "error": "Text gol."}
|
|
||||||
|
|
||||||
text = sanitize_for_supertonic(text)
|
text = sanitize_for_supertonic(text)
|
||||||
|
|
||||||
voice = voice.upper()
|
voice = voice.upper()
|
||||||
@@ -99,31 +138,94 @@ def synthesize(text: str, voice: str = DEFAULT_VOICE, lang: str = DEFAULT_LANG)
|
|||||||
body = e.response.text[:300]
|
body = e.response.text[:300]
|
||||||
# Fallback: dacă lang=ro eșuează, încearcă na (language-agnostic)
|
# Fallback: dacă lang=ro eșuează, încearcă na (language-agnostic)
|
||||||
if lang != "na":
|
if lang != "na":
|
||||||
return synthesize(text, voice=voice, lang="na")
|
return _synthesize_supertonic(text, voice=voice, lang="na")
|
||||||
return {"ok": False, "error": f"HTTP {e.response.status_code}: {body}"}
|
return {"ok": False, "error": f"HTTP {e.response.status_code}: {body}"}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"ok": False, "error": str(e)}
|
return {"ok": False, "error": str(e)}
|
||||||
|
|
||||||
# Salvează în fișier temp
|
# Salvează în fișier temp
|
||||||
|
return _write_temp_wav(resp.content)
|
||||||
|
|
||||||
|
|
||||||
|
def _synthesize_pockettts(text: str, entry: dict) -> dict:
|
||||||
|
"""POST multipart la serverul pocket-tts (:7789/tts). Ignoră lang (modelul e monolingv per deployment).
|
||||||
|
|
||||||
|
`entry` e intrarea din tts_voices.json (deja rezolvată de caller — evită un al doilea read+lock
|
||||||
|
al catalogului). state_path (voce clonată) -> încarcă fișierul ca voice_wav. voice_url (voce
|
||||||
|
predefinită, ex. 'alba') -> trimite voice_url. Nici unul -> nu trimite voice_wav/voice_url
|
||||||
|
(serverul alege default-ul limbii).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
_PocketTTSUnavailable: eșec tehnic (connect error/timeout/5xx) — caller-ul face fallback.
|
||||||
|
"""
|
||||||
|
state_path = entry.get("state_path")
|
||||||
|
voice_url = entry.get("voice_url")
|
||||||
|
|
||||||
|
data = {"text": text}
|
||||||
|
files = None
|
||||||
|
if state_path:
|
||||||
|
full_path = REPO_ROOT / state_path
|
||||||
|
if not full_path.exists():
|
||||||
|
return {"ok": False, "error": f"Fișier voce lipsă: {state_path}"}
|
||||||
|
files = {"voice_wav": (full_path.name, full_path.read_bytes())}
|
||||||
|
elif voice_url:
|
||||||
|
data["voice_url"] = voice_url
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fd, path = tempfile.mkstemp(prefix="echo-tts-", suffix=".wav")
|
resp = httpx.post(
|
||||||
with open(fd, "wb") as f:
|
f"{_config_get('tts.pockettts_url', 'http://127.0.0.1:7789')}/tts",
|
||||||
f.write(resp.content)
|
data=data,
|
||||||
return {"ok": True, "path": path, "size_bytes": len(resp.content)}
|
files=files,
|
||||||
except Exception as e:
|
timeout=60.0,
|
||||||
return {"ok": False, "error": f"Scriere fișier: {e}"}
|
)
|
||||||
|
except (httpx.ConnectError, httpx.TimeoutException) as e:
|
||||||
|
raise _PocketTTSUnavailable(str(e)) from e
|
||||||
|
|
||||||
|
if resp.status_code >= 500:
|
||||||
|
raise _PocketTTSUnavailable(f"HTTP {resp.status_code}: {resp.text[:300]}")
|
||||||
|
if resp.status_code >= 400:
|
||||||
|
return {"ok": False, "error": f"HTTP {resp.status_code}: {resp.text[:300]}"}
|
||||||
|
|
||||||
|
return _write_temp_wav(resp.content)
|
||||||
|
|
||||||
|
|
||||||
|
def synthesize(text: str, voice: str = DEFAULT_VOICE, lang: str = DEFAULT_LANG) -> dict:
|
||||||
|
"""Rutează sinteza pe engine-ul rezolvat din catalog (fallback la tts.default_engine).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{"ok": True, "path": "/tmp/echo-tts-xxx.wav", "size_bytes": N, "engine_used": "pockettts"}
|
||||||
|
{"ok": False, "error": "mesaj eroare", "engine_used": "..."}
|
||||||
|
"""
|
||||||
|
if not text or not text.strip():
|
||||||
|
return {"ok": False, "error": "Text gol."}
|
||||||
|
|
||||||
|
entry = _catalog_entry(voice) or {}
|
||||||
|
engine = entry.get("engine") or _config_get("tts.default_engine", DEFAULT_ENGINE)
|
||||||
|
|
||||||
|
engine_used = "supertonic"
|
||||||
|
if engine == "pockettts":
|
||||||
|
try:
|
||||||
|
result = _synthesize_pockettts(text, entry)
|
||||||
|
engine_used = "pockettts"
|
||||||
|
except _PocketTTSUnavailable:
|
||||||
|
result = _synthesize_supertonic(text, voice=DEFAULT_VOICE, lang=DEFAULT_LANG)
|
||||||
|
else:
|
||||||
|
result = _synthesize_supertonic(text, voice=voice, lang=lang)
|
||||||
|
|
||||||
|
result["engine_used"] = engine_used
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="Supertonic TTS CLI")
|
parser = argparse.ArgumentParser(description="Echo Core TTS CLI")
|
||||||
parser.add_argument("--text", required=True, help="Text de convertit în audio")
|
parser.add_argument("--text", required=True, help="Text de convertit în audio")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--voice", default=DEFAULT_VOICE,
|
"--voice", default=DEFAULT_VOICE,
|
||||||
help="Voce: M1-M5 (masculin) sau F1-F5 (feminin). Default: M1"
|
help="Voce din tts_voices.json (ex: M1-M5, F1-F5, 'Marius 1', 'alba'). Default: M2"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--lang", default=DEFAULT_LANG,
|
"--lang", default=DEFAULT_LANG,
|
||||||
help="Limbă (ro, en, na). Default: ro. Fallback automat la na dacă ro eșuează."
|
help="Limbă (ro, en, na) — folosită doar pe calea Supertonic. Default: ro."
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user