Skip to content
This repository was archived by the owner on Jul 27, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions PRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | 말하기 속도 | 모델 기본값 |
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down
11 changes: 9 additions & 2 deletions docs/data_model.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**:
Expand Down Expand Up @@ -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`.
Expand Down
7 changes: 4 additions & 3 deletions docs/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand All @@ -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.)

Expand Down Expand Up @@ -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.**

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
35 changes: 11 additions & 24 deletions src/supertone_cli/commands/tts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"}


Expand All @@ -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:
Expand Down Expand Up @@ -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 <file>, or pipe via stdin."
"No input provided. Pass text as argument, use --input <file>, or pipe via stdin."
)

if text:
Expand Down Expand Up @@ -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:
Expand All @@ -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)

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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."
),
Expand Down Expand Up @@ -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."),
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/test_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions tests/test_tts_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
21 changes: 16 additions & 5 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading