From d2e2b3ab6794f43bc2275c56bee5953a10e51531 Mon Sep 17 00:00:00 2001 From: pillip Date: Thu, 21 May 2026 16:07:55 +0900 Subject: [PATCH] feat: add supertonic_api_3 model (31 languages) - Bump supertone SDK to >=0.2.2 to expose the new supertonic_api_3 model and 8 additional languages (hr, lt, lv, sk, sl, sv, tr, uk; total now 31). - Add supertonic_api_3 to VALID_MODELS and extend the speed-only parameter guard so it applies to both supertonic_api_1 and supertonic_api_3. - Add validation test cases for supertonic_api_3 (pitch rejected, speed-only accepted). - Fix pre-existing flag mismatch in tests/integration/test_smoke.py: the CLI uses --format json, not --json. - Update README, PRD, docs/requirements.md, docs/data_model.md, and CHANGELOG (Unreleased) to document the new model. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 10 +++++++++ PRD.md | 4 ++-- README.md | 4 +++- docs/data_model.md | 11 ++++++++-- docs/requirements.md | 7 ++++--- pyproject.toml | 2 +- src/supertone_cli/commands/tts.py | 35 ++++++++++--------------------- tests/integration/test_smoke.py | 2 +- tests/test_tts_params.py | 9 ++++++++ uv.lock | 21 ++++++++++++++----- 10 files changed, 66 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 132e5fb..6976cd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- Model support: `supertonic_api_3` — 31 languages (adds `hr, lt, lv, sk, sl, sv, tr, uk` on top of the prior 23), `--speed` only for voice settings (same parameter constraints as `supertonic_api_1`). + +### Changed + +- Bump `supertone` minimum to `>=0.2.2` to expose `supertonic_api_3` and the expanded language enum. + ## [0.1.1] - 2026-05-10 ### Changed diff --git a/PRD.md b/PRD.md index 9dc8320..a621d62 100644 --- a/PRD.md +++ b/PRD.md @@ -100,8 +100,8 @@ Phase 1은 Supertone Python SDK를 래핑하는 Unix 파이프 친화적 CLI를 | 옵션 | 설명 | 기본값 | |------|------|--------| | `--voice` | 보이스 ID | config 기본값 또는 필수 | -| `--model` | 모델 선택 (sona_speech_1, supertonic_api_1, sona_speech_2, sona_speech_2_flash) | sona_speech_2 | -| `--lang` | 언어 코드 (ko, en, ja 등 23개) | ko | +| `--model` | 모델 선택 (sona_speech_1, supertonic_api_1, supertonic_api_3, sona_speech_2, sona_speech_2_flash, sona_speech_2t) | sona_speech_2 | +| `--lang` | 언어 코드 (ko, en, ja 등 31개; `supertonic_api_3` 사용 시 확장 언어 지원) | ko | | `--style` | 음성 스타일 | 모델 기본값 | | `--output-format` | 출력 오디오 포맷 (wav, mp3, ogg, flac, aiff) | wav | | `--speed` | 말하기 속도 | 모델 기본값 | diff --git a/README.md b/README.md index a6468ca..62d142e 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,9 @@ Human-readable output (tables, progress, errors) goes to **stderr**. Machine-rea ## Supported models -`sona_speech_1`, `sona_speech_2`, `sona_speech_2_flash`, `sona_speech_2t`, `supertonic_api_1`. Parameter compatibility varies by model; the CLI validates this and returns exit code `3` on mismatches. +`sona_speech_1`, `sona_speech_2`, `sona_speech_2_flash`, `sona_speech_2t`, `supertonic_api_1`, `supertonic_api_3`. Parameter compatibility varies by model; the CLI validates this and returns exit code `3` on mismatches. + +`supertonic_api_3` supports 31 languages and accepts only `--speed` for voice settings (other voice settings are rejected with exit code `3`). Streaming (`--stream`) currently requires `sona_speech_1`. diff --git a/docs/data_model.md b/docs/data_model.md index 808b621..c5b22fc 100644 --- a/docs/data_model.md +++ b/docs/data_model.md @@ -40,7 +40,7 @@ This is the only persistent data structure. It is a flat TOML file with no secti |-----|------|----------|---------|------------|-------------| | `api_key` | string | No (but needed for auth commands) | None | Non-empty string; must not be whitespace-only | Supertone API key | | `default_voice` | string | No | None | Non-empty string if present | Default voice ID for TTS commands | -| `default_model` | string | No | `"sona_speech_2"` (built-in fallback) | One of: `sona_speech_1`, `supertonic_api_1`, `sona_speech_2`, `sona_speech_2_flash` | Default TTS model | +| `default_model` | string | No | `"sona_speech_2"` (built-in fallback) | One of: `sona_speech_1`, `supertonic_api_1`, `supertonic_api_3`, `sona_speech_2`, `sona_speech_2_flash`, `sona_speech_2t` | Default TTS model | | `default_lang` | string | No | `"ko"` (built-in fallback) | Non-empty string if present | Default language code | **Constraints**: @@ -263,7 +263,14 @@ These are fixed value sets used across multiple modules. ### Model Names ```python -VALID_MODELS = {"sona_speech_1", "supertonic_api_1", "sona_speech_2", "sona_speech_2_flash"} +VALID_MODELS = { + "sona_speech_1", + "supertonic_api_1", + "supertonic_api_3", + "sona_speech_2", + "sona_speech_2_flash", + "sona_speech_2t", +} ``` Used for validation in `commands/tts.py` and `config set default_model`. diff --git a/docs/requirements.md b/docs/requirements.md index 25f46eb..27a3168 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -184,7 +184,7 @@ | Flag | Type | Default | Notes | |------|------|---------|-------| | `--voice` | string | `default_voice` from config or required | | -| `--model` | enum | `sona_speech_2` | `sona_speech_1`, `supertonic_api_1`, `sona_speech_2`, `sona_speech_2_flash` | +| `--model` | enum | `sona_speech_2` | `sona_speech_1`, `supertonic_api_1`, `supertonic_api_3`, `sona_speech_2`, `sona_speech_2_flash`, `sona_speech_2t` | | `--lang` | string | `ko` | Language code | | `--style` | string | model default | SDK style parameter | | `--output-format` | enum | `wav` | `wav`, `mp3`, `ogg`, `flac`, `aiff` | @@ -199,7 +199,8 @@ **Acceptance Criteria**: - Given `--model sona_speech_2_flash` and `--similarity` is provided, exit code is 3 and an error message states the parameter is not supported by that model. - Given `--model sona_speech_2_flash` and `--text-guidance` is provided, exit code is 3 and an error message states the parameter is not supported by that model. -- Given `--model supertonic_api_1` and any parameter other than `--speed` is provided (excluding `--voice`, `--lang`, `--output-format`), the unsupported parameter is silently ignored OR an exit-code-3 error is raised. (See Assumption A-4.) +- Given `--model supertonic_api_1` or `--model supertonic_api_3` and any parameter other than `--speed` is provided (excluding `--voice`, `--lang`, `--output-format`), the unsupported parameter is silently ignored OR an exit-code-3 error is raised. (See Assumption A-4.) +- Given `--model supertonic_api_3`, the SDK exposes 31 languages (`ar, bg, cs, da, de, el, en, es, et, fi, fr, hi, hr, hu, id, it, ja, ko, lt, lv, nl, pl, pt, ro, ru, sk, sl, sv, tr, uk, vi`); `--lang` is validated against the SDK enum. - Given `--stream` and model is not `sona_speech_1`, exit code is 3. - Given `--include-phonemes true`, phoneme data is included in the response and written alongside the audio output. (See Assumption A-5 for output format.) @@ -479,7 +480,7 @@ The following are explicitly excluded from Phase 1: **A-3**: `supertone config get api_key` behavior is not defined regarding masking. This document assumes the full key is returned unmasked since the command is explicit and the user is authenticated at the OS level by the `600` file permission. **Verify with stakeholder — masking may be preferred.** -**A-4**: The PRD states `supertonic_api_1` supports only `--speed` for audio adjustment. The behavior when other audio parameters (`--pitch`, `--similarity`, etc.) are passed with this model is not specified. This document flags this as ambiguous. **Recommend: raise exit code 3 with a clear message listing unsupported parameters for the model.** +**A-4**: The PRD states `supertonic_api_1` (and `supertonic_api_3`) supports only `--speed` for audio adjustment. The behavior when other audio parameters (`--pitch`, `--similarity`, etc.) are passed with these models is not specified. This document flags this as ambiguous. **Recommend: raise exit code 3 with a clear message listing unsupported parameters for the model.** **A-5**: The behavior of `--include-phonemes true` regarding output is not specified. The PRD does not state whether phoneme data is printed to stdout, written to a sidecar file, or embedded in the JSON response. **Verify with stakeholder before implementing.** diff --git a/pyproject.toml b/pyproject.toml index 070418b..1fd531d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ dependencies = [ "typer>=0.9,<1.0", "rich>=13.0", "tomli_w>=1.0", - "supertone>=0.2.1,<0.3", + "supertone>=0.2.2,<0.3", ] [project.optional-dependencies] diff --git a/src/supertone_cli/commands/tts.py b/src/supertone_cli/commands/tts.py index 9050a8a..38fe2db 100644 --- a/src/supertone_cli/commands/tts.py +++ b/src/supertone_cli/commands/tts.py @@ -21,6 +21,7 @@ VALID_MODELS = { "sona_speech_1", "supertonic_api_1", + "supertonic_api_3", "sona_speech_2", "sona_speech_2_flash", "sona_speech_2t", @@ -31,6 +32,7 @@ # Model-parameter compatibility matrix _FLASH_DISALLOWED = {"similarity", "text_guidance"} _SUPERTONIC_ALLOWED = {"speed"} +_SUPERTONIC_MODELS = {"supertonic_api_1", "supertonic_api_3"} _STREAM_MODELS = {"sona_speech_1"} @@ -48,13 +50,11 @@ def validate_params(model: str, **kwargs: object) -> None: if bad: raise InputError(f"Not supported by {model}: {', '.join(sorted(bad))}") - if model == "supertonic_api_1": + if model in _SUPERTONIC_MODELS: bad = set(params) - _SUPERTONIC_ALLOWED - {"stream"} if bad: raise InputError( - f"Not supported by {model}: " - f"{', '.join(sorted(bad))}. " - f"Only speed is supported." + f"Not supported by {model}: {', '.join(sorted(bad))}. Only speed is supported." ) if params.get("stream") and model not in _STREAM_MODELS: @@ -85,8 +85,7 @@ def _resolve_text( if not sources and not stdin_has_data: raise InputError( - "No input provided. Pass text as argument, " - "use --input , or pipe via stdin." + "No input provided. Pass text as argument, use --input , or pipe via stdin." ) if text: @@ -271,9 +270,7 @@ def _run_tts( # noqa: PLR0913 stream=stream if stream else None, ) - voice_settings = _build_settings_kwargs( - speed, pitch, pitch_variance, similarity, text_guidance - ) + voice_settings = _build_settings_kwargs(speed, pitch, pitch_variance, similarity, text_guidance) # Batch mode: directory input + outdir if _is_batch_input(input) and outdir: @@ -291,9 +288,7 @@ def _run_tts( # noqa: PLR0913 return if format == "json" and output == "-": - raise InputError( - "Cannot use --format json with --output -: both write to stdout." - ) + raise InputError("Cannot use --format json with --output -: both write to stdout.") resolved_text = _resolve_text(text, input) @@ -350,9 +345,7 @@ def register_tts_command(app: typer.Typer) -> None: @app.command("tts") def tts_cmd( # noqa: PLR0913 text: Optional[str] = typer.Argument(None, help="Text to synthesize."), - input: Optional[str] = typer.Option( - None, "--input", "-i", help="Path to text file." - ), + input: Optional[str] = typer.Option(None, "--input", "-i", help="Path to text file."), output: Optional[str] = typer.Option( None, "--output", @@ -394,9 +387,7 @@ def tts_cmd( # noqa: PLR0913 pitch_variance: Optional[float] = typer.Option( None, "--pitch-variance", help="Pitch variance." ), - similarity: Optional[float] = typer.Option( - None, "--similarity", help="Voice similarity." - ), + similarity: Optional[float] = typer.Option(None, "--similarity", help="Voice similarity."), text_guidance: Optional[float] = typer.Option( None, "--text-guidance", help="Text guidance." ), @@ -429,12 +420,8 @@ def register_predict_command(app: typer.Typer) -> None: @app.command("tts-predict") def predict_cmd( - text: Optional[str] = typer.Argument( - None, help="Text to predict duration for." - ), - input: Optional[str] = typer.Option( - None, "--input", "-i", help="Path to text file." - ), + text: Optional[str] = typer.Argument(None, help="Text to predict duration for."), + input: Optional[str] = typer.Option(None, "--input", "-i", help="Path to text file."), voice: Optional[str] = typer.Option(None, "--voice", "-v", help="Voice ID."), model: Optional[str] = typer.Option(None, "--model", "-m", help="TTS model."), lang: Optional[str] = typer.Option(None, "--lang", "-l", help="Language code."), diff --git a/tests/integration/test_smoke.py b/tests/integration/test_smoke.py index d639ce9..ce55041 100644 --- a/tests/integration/test_smoke.py +++ b/tests/integration/test_smoke.py @@ -25,7 +25,7 @@ @pytest.mark.skipif(not _HAS_KEY, reason="SUPERTONE_API_KEY not set") def test_voices_list_smoke(): """Smoke test: voices list returns valid JSON with at least one voice.""" - result = runner.invoke(app, ["voices", "list", "--json"]) + result = runner.invoke(app, ["voices", "list", "--format", "json"]) assert result.exit_code == 0, f"CLI failed: {result.output}" data = json.loads(result.output) assert isinstance(data, list) diff --git a/tests/test_tts_params.py b/tests/test_tts_params.py index c14f1f3..e983f6e 100644 --- a/tests/test_tts_params.py +++ b/tests/test_tts_params.py @@ -30,6 +30,15 @@ def test_supertonic_rejects_pitch(): validate_params("supertonic_api_1", pitch=0.5) +def test_supertonic_api_3_rejects_pitch(): + with pytest.raises(InputError): + validate_params("supertonic_api_3", pitch=0.5) + + +def test_supertonic_api_3_allows_speed_only(): + validate_params("supertonic_api_3", speed=1.1) + + def test_sona2_allows_speed_and_pitch(): validate_params("sona_speech_2", speed=1.2, pitch=0.5) diff --git a/uv.lock b/uv.lock index d5cfd77..66cae69 100644 --- a/uv.lock +++ b/uv.lock @@ -204,6 +204,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, ] +[[package]] +name = "dotenv" +version = "0.9.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dotenv" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -552,17 +563,17 @@ wheels = [ [[package]] name = "supertone" -version = "0.2.1" +version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "dotenv" }, { name = "httpcore" }, { name = "httpx" }, { name = "pydantic" }, - { name = "python-dotenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/43/dab7839e9c554876221c4befdd138954da2346be0e867fb736a59c28319c/supertone-0.2.1.tar.gz", hash = "sha256:13c507c912493f68bc327285328851d96f7e33dfe9974c2394982e6de2bbabd4", size = 67461, upload-time = "2026-05-10T13:00:58.505Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/53/5999df4359fac701ea7bfc1bdf71bab98151d7ad72905000e34cedb589dd/supertone-0.2.2.tar.gz", hash = "sha256:02a193bc1fa5dc2020da3aed8b44cc1feab650030428434f4b60ce7189efa790", size = 67803, upload-time = "2026-05-21T06:17:03.757Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/75/993496a0c0ef8b015767243981f4db9e0af8ad268bfcd3fe283024a55cb3/supertone-0.2.1-py3-none-any.whl", hash = "sha256:436585dc8f5458b5eae26067ad8aed54b8d718e5bd33edafb0b6980c870bd898", size = 102317, upload-time = "2026-05-10T13:00:57.144Z" }, + { url = "https://files.pythonhosted.org/packages/91/43/d5a37077cfe7b6cc4441e6a47e3ee11e7061a16515ef2152041ed53c20f5/supertone-0.2.2-py3-none-any.whl", hash = "sha256:7b17df966ca1b871b486b18e810e2fb321f640a7f7141d9d84e3e7e16bd9516b", size = 102488, upload-time = "2026-05-21T06:17:02.363Z" }, ] [[package]] @@ -594,7 +605,7 @@ requires-dist = [ { name = "rich", specifier = ">=13.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.4" }, { name = "sounddevice", marker = "extra == 'stream'", specifier = ">=0.4" }, - { name = "supertone", specifier = ">=0.2.1,<0.3" }, + { name = "supertone", specifier = ">=0.2.2,<0.3" }, { name = "tomli-w", specifier = ">=1.0" }, { name = "typer", specifier = ">=0.9,<1.0" }, ]