diff --git a/README.md b/README.md index f732655..557c75d 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ pip install fish-audio-sdk[utils] ## Authentication -Get your API key from [fish.audio/app/api-keys](https://fish.audio/app/api-keys): +Get your API key from [fish.audio/app/api-keys](https://fish.audio/app/api-keys/?utm_source=github&utm_medium=sdk-readme&utm_content=python): ```bash export FISH_API_KEY=your_api_key_here @@ -93,6 +93,18 @@ production_audio = client.tts.convert( text="Production speech", model="s2.1-pro", ) + +# Free to use, including on a new account with no API credit +free_audio = client.tts.convert( + text="Free speech", + model="s2.1-pro-free", +) + +# Drama 3: our most expressive model (uses API credit) +drama_audio = client.tts.convert( + text="Expressive speech", + model="drama-3", +) ``` **With custom voice:** diff --git a/src/fishaudio/types/shared.py b/src/fishaudio/types/shared.py index a160e65..0628f9f 100644 --- a/src/fishaudio/types/shared.py +++ b/src/fishaudio/types/shared.py @@ -22,7 +22,7 @@ class PaginatedResponse(BaseModel, Generic[T]): # Model types -Model = Literal["s1", "s2-pro", "s2.1-pro"] +Model = Literal["s1", "s2-pro", "s2.1-pro", "s2.1-pro-free", "drama-3"] # Deprecated models DEPRECATED_MODELS = {"speech-1.5", "speech-1.6"} diff --git a/src/fishaudio/types/tts.py b/src/fishaudio/types/tts.py index a6bfbc3..8799cbd 100644 --- a/src/fishaudio/types/tts.py +++ b/src/fishaudio/types/tts.py @@ -2,7 +2,7 @@ from typing import Annotated, Literal, Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, field_serializer from .shared import AudioFormat, LatencyMode @@ -157,6 +157,11 @@ class TTSRequest(BaseModel): condition_on_previous_chunks: bool = True early_stop_threshold: float = 1.0 + @field_serializer("opus_bitrate") + def _opus_bitrate_bps(self, value: int) -> int: + # The API takes opus_bitrate in bps (24000, 32000, ...); -1000 means auto. + return value * 1000 if value > 0 else value + # WebSocket event types for streaming TTS class StartEvent(BaseModel): diff --git a/tests/unit/test_tts.py b/tests/unit/test_tts.py index f6075a5..7cabc48 100644 --- a/tests/unit/test_tts.py +++ b/tests/unit/test_tts.py @@ -87,6 +87,30 @@ def test_convert_basic(self, tts_client, mock_client_wrapper): call_args = mock_client_wrapper.request.call_args assert call_args[1]["headers"]["model"] == "s2.1-pro" + def test_convert_with_free_model(self, tts_client, mock_client_wrapper): + """Test the free s2.1-pro-free model is sent as the model header.""" + mock_response = Mock() + mock_response.iter_bytes.return_value = iter([b"audio"]) + mock_client_wrapper.request.return_value = mock_response + + audio = tts_client.convert(text="Hello world", model="s2.1-pro-free") + + assert audio == b"audio" + call_args = mock_client_wrapper.request.call_args + assert call_args[1]["headers"]["model"] == "s2.1-pro-free" + + def test_convert_with_drama_3(self, tts_client, mock_client_wrapper): + """Test the drama-3 model is sent as the model header.""" + mock_response = Mock() + mock_response.iter_bytes.return_value = iter([b"audio"]) + mock_client_wrapper.request.return_value = mock_response + + audio = tts_client.convert(text="Hello world", model="drama-3") + + assert audio == b"audio" + call_args = mock_client_wrapper.request.call_args + assert call_args[1]["headers"]["model"] == "drama-3" + def test_convert_with_reference_id(self, tts_client, mock_client_wrapper): """Test TTS with reference voice ID.""" mock_response = Mock() diff --git a/tests/unit/test_types.py b/tests/unit/test_types.py index cfc8446..d6f760a 100644 --- a/tests/unit/test_types.py +++ b/tests/unit/test_types.py @@ -173,3 +173,11 @@ def test_voice_with_unlist_visibility(self, sample_voice_response): sample_voice_response["visibility"] = "unlist" voice = Voice.model_validate(sample_voice_response) assert voice.visibility == "unlist" + + +def test_tts_request_sends_opus_bitrate_in_bps(): + assert TTSRequest(text="hi", format="opus").model_dump()["opus_bitrate"] == 32000 + assert TTSRequest(text="hi", opus_bitrate=64).model_dump()["opus_bitrate"] == 64000 + assert ( + TTSRequest(text="hi", opus_bitrate=-1000).model_dump()["opus_bitrate"] == -1000 + )