Skip to content

Commit 3ac2b9d

Browse files
authored
feat: replace mypy with ty (#73)
* chore(deps): add ty package version 0.0.2 to dependencies * chore: update type hints and add typing-extensions dependency * chore: remove mypy dependency and update CI to use ty for type checking * refactor: update type hints to use Iterable instead of Iterator for audio parameters * feat: add example ipynb * fix: lint with import clarification * fix: clear ipynb
1 parent ac5f010 commit 3ac2b9d

File tree

8 files changed

+248
-74
lines changed

8 files changed

+248
-74
lines changed

.github/workflows/python.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ jobs:
3232
python-version: "3"
3333
- name: Install dependencies
3434
run: uv sync
35-
- name: Run mypy
36-
run: uv run mypy src/fishaudio --ignore-missing-imports
35+
- name: Run ty
36+
run: uv run ty check
3737

3838
test:
3939
name: Test Python ${{ matrix.python-version }}

examples/getting_started.ipynb

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Fish Audio SDK - Getting Started\n",
8+
"\n",
9+
"This notebook demonstrates the basic usage of the Fish Audio Python SDK:\n",
10+
"- Initialize the client\n",
11+
"- Convert text to speech\n",
12+
"- Save and play audio\n",
13+
"- Use different voices"
14+
]
15+
},
16+
{
17+
"cell_type": "markdown",
18+
"metadata": {},
19+
"source": [
20+
"## Setup\n",
21+
"\n",
22+
"First, install the SDK and set your API key:\n",
23+
"\n",
24+
"```bash\n",
25+
"pip install fishaudio\n",
26+
"export FISH_API_KEY=\"your_api_key\"\n",
27+
"```\n",
28+
"\n",
29+
"Or create a `.env` file with `FISH_API_KEY=your_api_key`"
30+
]
31+
},
32+
{
33+
"cell_type": "code",
34+
"execution_count": null,
35+
"metadata": {
36+
"ExecuteTime": {
37+
"end_time": "2025-12-17T02:19:01.713412Z",
38+
"start_time": "2025-12-17T02:19:01.692232Z"
39+
}
40+
},
41+
"outputs": [],
42+
"source": "from dotenv import load_dotenv\nfrom fishaudio import FishAudio\nfrom fishaudio.utils import play\n# from fishaudio.utils import save # Uncomment if saving audio to file\n\nload_dotenv()\n\nclient = FishAudio()"
43+
},
44+
{
45+
"cell_type": "markdown",
46+
"metadata": {},
47+
"source": [
48+
"## Simple Text-to-Speech\n",
49+
"\n",
50+
"Convert text to speech and play it directly in the notebook."
51+
]
52+
},
53+
{
54+
"cell_type": "code",
55+
"execution_count": null,
56+
"metadata": {
57+
"ExecuteTime": {
58+
"end_time": "2025-12-17T02:19:02.811072Z",
59+
"start_time": "2025-12-17T02:19:01.715025Z"
60+
}
61+
},
62+
"outputs": [],
63+
"source": [
64+
"audio = client.tts.convert(text=\"Hello! Welcome to Fish Audio.\")\n",
65+
"\n",
66+
"play(audio, notebook=True)"
67+
]
68+
},
69+
{
70+
"cell_type": "markdown",
71+
"metadata": {},
72+
"source": [
73+
"## Save Audio to File\n",
74+
"\n",
75+
"Save the generated audio to an MP3 file."
76+
]
77+
},
78+
{
79+
"cell_type": "code",
80+
"execution_count": null,
81+
"metadata": {
82+
"ExecuteTime": {
83+
"end_time": "2025-12-17T02:19:02.822441Z",
84+
"start_time": "2025-12-17T02:19:02.818578Z"
85+
}
86+
},
87+
"outputs": [],
88+
"source": [
89+
"# audio = client.tts.convert(text=\"This audio will be saved to a file.\")\n",
90+
"# save(audio, \"output.mp3\")"
91+
]
92+
},
93+
{
94+
"cell_type": "markdown",
95+
"metadata": {},
96+
"source": [
97+
"## Using a Specific Voice\n",
98+
"\n",
99+
"Use `reference_id` to specify a voice model from your Fish Audio account."
100+
]
101+
},
102+
{
103+
"cell_type": "code",
104+
"execution_count": null,
105+
"metadata": {
106+
"ExecuteTime": {
107+
"end_time": "2025-12-17T02:19:02.826568Z",
108+
"start_time": "2025-12-17T02:19:02.822894Z"
109+
}
110+
},
111+
"outputs": [],
112+
"source": [
113+
"# Replace with your voice model ID\n",
114+
"# audio = client.tts.convert(\n",
115+
"# text=\"Hello from a custom voice!\",\n",
116+
"# reference_id=\"your-voice-model-id\"\n",
117+
"# )\n",
118+
"# play(audio, notebook=True)"
119+
]
120+
},
121+
{
122+
"cell_type": "markdown",
123+
"metadata": {},
124+
"source": [
125+
"## Streaming Audio\n",
126+
"\n",
127+
"For longer text, use `stream()` to process audio chunks as they arrive."
128+
]
129+
},
130+
{
131+
"cell_type": "code",
132+
"execution_count": null,
133+
"metadata": {
134+
"ExecuteTime": {
135+
"end_time": "2025-12-17T02:19:03.824681Z",
136+
"start_time": "2025-12-17T02:19:02.826991Z"
137+
}
138+
},
139+
"outputs": [],
140+
"source": [
141+
"stream = client.tts.stream(text=\"This is a longer piece of text that will be streamed.\")\n",
142+
"audio = stream.collect()\n",
143+
"\n",
144+
"play(audio, notebook=True)"
145+
]
146+
},
147+
{
148+
"cell_type": "markdown",
149+
"metadata": {},
150+
"source": [
151+
"## Check Account Credits"
152+
]
153+
},
154+
{
155+
"cell_type": "code",
156+
"execution_count": null,
157+
"metadata": {
158+
"ExecuteTime": {
159+
"end_time": "2025-12-17T02:19:03.897563Z",
160+
"start_time": "2025-12-17T02:19:03.833734Z"
161+
}
162+
},
163+
"outputs": [],
164+
"source": [
165+
"credits = client.account.get_credits()\n",
166+
"print(f\"Remaining credits: {credits.credit}\")"
167+
]
168+
}
169+
],
170+
"metadata": {
171+
"kernelspec": {
172+
"display_name": "Python 3",
173+
"language": "python",
174+
"name": "python3"
175+
},
176+
"language_info": {
177+
"name": "python",
178+
"version": "3.11.0"
179+
}
180+
},
181+
"nbformat": 4,
182+
"nbformat_minor": 4
183+
}

pyproject.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ dependencies = [
1111
"ormsgpack>=1.5.0",
1212
"pydantic>=2.9.1",
1313
"httpx-ws>=0.6.2",
14+
"typing-extensions>=4.15.0",
1415
]
1516
requires-python = ">=3.9"
1617
readme = "README.md"
@@ -55,14 +56,15 @@ asyncio_mode = "auto"
5556

5657
[dependency-groups]
5758
dev = [
58-
"mypy>=1.14.1",
59+
"fish-audio-sdk[utils]",
5960
"pydoc-markdown>=4.8.2",
6061
"pytest>=8.3.5",
6162
"pytest-asyncio>=0.24.0",
6263
"pytest-cov>=5.0.0",
6364
"pytest-rerunfailures>=16.0.1",
6465
"python-dotenv>=1.0.1",
6566
"ruff>=0.14.3",
67+
"ty>=0.0.2",
6668
]
6769

6870
[[tool.pydoc-markdown.loaders]]
@@ -80,3 +82,6 @@ pages = [
8082
{title = "Utils", name="fishaudio/utils", contents = ["fishaudio.utils.*"] },
8183
{title = "Exceptions", name="fishaudio/exceptions", contents = ["fishaudio.exceptions.*"] },
8284
]
85+
86+
[tool.uv.sources]
87+
fish-audio-sdk = { workspace = true }

src/fish_audio_sdk/io.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
AsyncGenerator,
77
Awaitable,
88
Callable,
9-
Concatenate,
109
Generator,
1110
Generic,
12-
ParamSpec,
1311
TypeVar,
1412
)
1513

14+
from typing_extensions import Concatenate, ParamSpec
15+
1616
import httpx
1717
import httpx._client
1818
import httpx._types

src/fishaudio/utils/play.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import io
44
import subprocess
5-
from typing import Iterator, Union
5+
from typing import Iterable, Union
66

77
from ..exceptions import DependencyError
88

@@ -17,7 +17,7 @@ def _is_installed(command: str) -> bool:
1717

1818

1919
def play(
20-
audio: Union[bytes, Iterator[bytes]],
20+
audio: Union[bytes, Iterable[bytes]],
2121
*,
2222
notebook: bool = False,
2323
use_ffmpeg: bool = True,
@@ -26,7 +26,7 @@ def play(
2626
Play audio using various playback methods.
2727
2828
Args:
29-
audio: Audio bytes or iterator of bytes
29+
audio: Audio bytes or iterable of bytes
3030
notebook: Use Jupyter notebook playback (IPython.display.Audio)
3131
use_ffmpeg: Use ffplay for playback (default, falls back to sounddevice)
3232
@@ -51,13 +51,13 @@ def play(
5151
```
5252
"""
5353
# Consolidate iterator to bytes
54-
if isinstance(audio, Iterator):
54+
if not isinstance(audio, bytes):
5555
audio = b"".join(audio)
5656

5757
# Notebook mode
5858
if notebook:
5959
try:
60-
from IPython.display import Audio, display
60+
from IPython.display import Audio, display # ty: ignore[unresolved-import]
6161

6262
display(Audio(audio, rate=44100, autoplay=True))
6363
return

src/fishaudio/utils/save.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
"""Audio saving utility."""
22

3-
from typing import Iterator, Union
3+
from typing import Iterable, Union
44

55

6-
def save(audio: Union[bytes, Iterator[bytes]], filename: str) -> None:
6+
def save(audio: Union[bytes, Iterable[bytes]], filename: str) -> None:
77
"""
88
Save audio to a file.
99
1010
Args:
11-
audio: Audio bytes or iterator of bytes
11+
audio: Audio bytes or iterable of bytes
1212
filename: Path to save the audio file
1313
1414
Examples:
@@ -27,7 +27,7 @@ def save(audio: Union[bytes, Iterator[bytes]], filename: str) -> None:
2727
```
2828
"""
2929
# Consolidate iterator to bytes if needed
30-
if isinstance(audio, Iterator):
30+
if not isinstance(audio, bytes):
3131
audio = b"".join(audio)
3232

3333
# Write to file

tests/unit/test_utils.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,24 @@ def test_play_ffmpeg_not_installed(self):
6262
assert "ffplay" in str(exc_info.value)
6363

6464
def test_play_sounddevice_mode(self):
65-
"""Test using sounddevice directly."""
66-
# Since we can't easily test sounddevice without installing it,
67-
# just test that the error is raised when it's not available
68-
with patch(
69-
"subprocess.run", side_effect=[subprocess.CalledProcessError(1, "which")]
70-
):
65+
"""Test using sounddevice for playback."""
66+
mock_sd = Mock()
67+
mock_sf = Mock()
68+
mock_sf.read.return_value = ([0.1, 0.2], 44100)
69+
70+
with patch.dict("sys.modules", {"sounddevice": mock_sd, "soundfile": mock_sf}):
71+
play(b"audio", use_ffmpeg=False)
72+
73+
mock_sf.read.assert_called_once()
74+
mock_sd.play.assert_called_once()
75+
mock_sd.wait.assert_called_once()
76+
77+
def test_play_sounddevice_not_installed(self):
78+
"""Test error when sounddevice not installed."""
79+
with patch.dict("sys.modules", {"sounddevice": None, "soundfile": None}):
7180
with pytest.raises(DependencyError) as exc_info:
7281
play(b"audio", use_ffmpeg=False)
7382

74-
# Should mention sounddevice in the error
7583
assert "sounddevice" in str(exc_info.value) or "fishaudio[utils]" in str(
7684
exc_info.value
7785
)

0 commit comments

Comments
 (0)