├── .github └── workflows │ └── ci.yml ├── .gitignore ├── README.md ├── pdm.lock ├── pyproject.toml ├── src └── fish_audio_sdk │ ├── __init__.py │ ├── apis.py │ ├── exceptions.py │ ├── io.py │ ├── schemas.py │ └── websocket.py └── tests ├── __init__.py ├── conftest.py ├── hello.mp3 ├── test_apis.py └── test_websocket.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | 3 | on: ["push", "pull_request"] 4 | 5 | jobs: 6 | tests: 7 | name: "Python ${{ matrix.python-version }} ${{ matrix.os }}" 8 | runs-on: "${{ matrix.os }}" 9 | strategy: 10 | matrix: 11 | python-version: ["3.10", "3.11", "3.12"] 12 | os: [ubuntu-latest] 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - uses: pdm-project/setup-pdm@v4 17 | name: Setup Python and PDM 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | 21 | - name: Install dependencies 22 | run: | 23 | pdm sync -v -dG dev 24 | 25 | - name: Tests 26 | run: pdm run pytest tests -o log_cli=true -o log_cli_level=DEBUG 27 | env: 28 | APIKEY: ${{ secrets.APIKEY }} 29 | 30 | publish: 31 | needs: tests 32 | if: startsWith(github.ref, 'refs/tags/') 33 | 34 | name: Publish to PyPI 35 | runs-on: ubuntu-latest 36 | environment: release 37 | permissions: 38 | id-token: write 39 | steps: 40 | - uses: actions/checkout@v4 41 | 42 | - uses: pdm-project/setup-pdm@v4 43 | name: Setup Python and PDM 44 | with: 45 | python-version: "3.10" 46 | architecture: x64 47 | version: 2.10.4 48 | 49 | - name: Build package distributions 50 | run: | 51 | pdm build 52 | 53 | - name: Publish package distributions to PyPI 54 | uses: pypa/gh-action-pypi-publish@release/v1 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm-project.org/#use-with-ide 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fish Audio Python SDK 2 | 3 | To provide convenient Python program integration for https://docs.fish.audio. 4 | 5 | ## Install 6 | 7 | ```bash 8 | pip install fish-audio-sdk 9 | ``` 10 | 11 | ## Usage 12 | 13 | Initialize a `Session` to use APIs. All APIs have synchronous and asynchronous versions. If you want to use the asynchronous version of the API, you only need to rewrite the original `session.api_call(...)` to `session.api_call.awaitable(...)`. 14 | 15 | ```python 16 | from fish_audio_sdk import Session 17 | 18 | session = Session("your_api_key") 19 | ``` 20 | 21 | Sometimes, you may need to change our endpoint to another address. You can use 22 | 23 | ```python 24 | from fish_audio_sdk import Session 25 | 26 | session = Session("your_api_key", base_url="https://your-proxy-domain") 27 | ``` 28 | 29 | ### Text to speech 30 | 31 | ```python 32 | from fish_audio_sdk import Session, TTSRequest 33 | 34 | session = Session("your_api_key") 35 | 36 | with open("r.mp3", "wb") as f: 37 | for chunk in session.tts(TTSRequest(text="Hello, world!")): 38 | f.write(chunk) 39 | ``` 40 | 41 | Or use async version: 42 | 43 | ```python 44 | import asyncio 45 | import aiofiles 46 | 47 | from fish_audio_sdk import Session, TTSRequest 48 | 49 | session = Session("your_api_key") 50 | 51 | 52 | async def main(): 53 | async with aiofiles.open("r.mp3", "wb") as f: 54 | async for chunk in session.tts.awaitable( 55 | TTSRequest(text="Hello, world!"), 56 | ): 57 | await f.write(chunk) 58 | 59 | 60 | asyncio.run(main()) 61 | ``` 62 | 63 | #### Reference Audio 64 | 65 | ```python 66 | from fish_audio_sdk import TTSRequest 67 | 68 | TTSRequest( 69 | text="Hello, world!", 70 | reference_id="your_model_id", 71 | ) 72 | ``` 73 | 74 | Or just use `ReferenceAudio` in `TTSRequest`: 75 | 76 | ```python 77 | from fish_audio_sdk import TTSRequest, ReferenceAudio 78 | 79 | TTSRequest( 80 | text="Hello, world!", 81 | references=[ 82 | ReferenceAudio( 83 | audio=audio_file.read(), 84 | text="reference audio text", 85 | ) 86 | ], 87 | ) 88 | ``` 89 | 90 | ### List models 91 | 92 | ```python 93 | models = session.list_models() 94 | print(models) 95 | ``` 96 | 97 | Or use async version: 98 | 99 | ```python 100 | import asyncio 101 | 102 | 103 | async def main(): 104 | models = await session.list_models.awaitable() 105 | print(models) 106 | 107 | 108 | asyncio.run(main()) 109 | ``` 110 | 111 | 112 | 113 | ### Get a model info by id 114 | 115 | ```python 116 | model = session.get_model("your_model_id") 117 | print(model) 118 | ``` 119 | 120 | Or use async version: 121 | 122 | ```python 123 | import asyncio 124 | 125 | 126 | async def main(): 127 | model = await session.get_model.awaitable("your_model_id") 128 | print(model) 129 | 130 | 131 | asyncio.run(main()) 132 | ``` 133 | 134 | ### Create a model 135 | 136 | ```python 137 | model = session.create_model( 138 | title="test", 139 | description="test", 140 | voices=[voice_file.read(), other_voice_file.read()], 141 | cover_image=image_file.read(), 142 | ) 143 | print(model) 144 | ``` 145 | 146 | Or use async version: 147 | 148 | ```python 149 | import asyncio 150 | 151 | 152 | async def main(): 153 | model = await session.create_model.awaitable( 154 | title="test", 155 | description="test", 156 | voices=[voice_file.read(), other_voice_file.read()], 157 | cover_image=image_file.read(), 158 | ) 159 | print(model) 160 | 161 | 162 | asyncio.run(main()) 163 | ``` 164 | 165 | 166 | ### Delete a model 167 | 168 | ```python 169 | session.delete_model("your_model_id") 170 | ``` 171 | 172 | Or use async version: 173 | 174 | ```python 175 | import asyncio 176 | 177 | 178 | async def main(): 179 | await session.delete_model.awaitable("your_model_id") 180 | 181 | 182 | asyncio.run(main()) 183 | ``` 184 | -------------------------------------------------------------------------------- /pdm.lock: -------------------------------------------------------------------------------- 1 | # This file is @generated by PDM. 2 | # It is not intended for manual editing. 3 | 4 | [metadata] 5 | groups = ["default", "dev"] 6 | strategy = ["cross_platform", "inherit_metadata"] 7 | lock_version = "4.5.0" 8 | content_hash = "sha256:5fd761c1f74135159e4442da8af5cca85cfafe9eedff8e88c0145f7d7d41c467" 9 | 10 | [[metadata.targets]] 11 | requires_python = ">=3.10" 12 | 13 | [[package]] 14 | name = "annotated-types" 15 | version = "0.7.0" 16 | requires_python = ">=3.8" 17 | summary = "Reusable constraint types to use with typing.Annotated" 18 | groups = ["default"] 19 | dependencies = [ 20 | "typing-extensions>=4.0.0; python_version < \"3.9\"", 21 | ] 22 | files = [ 23 | {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, 24 | {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, 25 | ] 26 | 27 | [[package]] 28 | name = "anyio" 29 | version = "4.6.2.post1" 30 | requires_python = ">=3.9" 31 | summary = "High level compatibility layer for multiple asynchronous event loop implementations" 32 | groups = ["default"] 33 | dependencies = [ 34 | "exceptiongroup>=1.0.2; python_version < \"3.11\"", 35 | "idna>=2.8", 36 | "sniffio>=1.1", 37 | "typing-extensions>=4.1; python_version < \"3.11\"", 38 | ] 39 | files = [ 40 | {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, 41 | {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, 42 | ] 43 | 44 | [[package]] 45 | name = "certifi" 46 | version = "2024.8.30" 47 | requires_python = ">=3.6" 48 | summary = "Python package for providing Mozilla's CA Bundle." 49 | groups = ["default"] 50 | files = [ 51 | {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, 52 | {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, 53 | ] 54 | 55 | [[package]] 56 | name = "colorama" 57 | version = "0.4.6" 58 | requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 59 | summary = "Cross-platform colored terminal text." 60 | groups = ["dev"] 61 | marker = "sys_platform == \"win32\"" 62 | files = [ 63 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 64 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 65 | ] 66 | 67 | [[package]] 68 | name = "exceptiongroup" 69 | version = "1.2.2" 70 | requires_python = ">=3.7" 71 | summary = "Backport of PEP 654 (exception groups)" 72 | groups = ["default", "dev"] 73 | marker = "python_version < \"3.11\"" 74 | files = [ 75 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 76 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 77 | ] 78 | 79 | [[package]] 80 | name = "h11" 81 | version = "0.14.0" 82 | requires_python = ">=3.7" 83 | summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 84 | groups = ["default"] 85 | dependencies = [ 86 | "typing-extensions; python_version < \"3.8\"", 87 | ] 88 | files = [ 89 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 90 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 91 | ] 92 | 93 | [[package]] 94 | name = "httpcore" 95 | version = "1.0.7" 96 | requires_python = ">=3.8" 97 | summary = "A minimal low-level HTTP client." 98 | groups = ["default"] 99 | dependencies = [ 100 | "certifi", 101 | "h11<0.15,>=0.13", 102 | ] 103 | files = [ 104 | {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, 105 | {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, 106 | ] 107 | 108 | [[package]] 109 | name = "httpx" 110 | version = "0.28.0" 111 | requires_python = ">=3.8" 112 | summary = "The next generation HTTP client." 113 | groups = ["default"] 114 | dependencies = [ 115 | "anyio", 116 | "certifi", 117 | "httpcore==1.*", 118 | "idna", 119 | ] 120 | files = [ 121 | {file = "httpx-0.28.0-py3-none-any.whl", hash = "sha256:dc0b419a0cfeb6e8b34e85167c0da2671206f5095f1baa9663d23bcfd6b535fc"}, 122 | {file = "httpx-0.28.0.tar.gz", hash = "sha256:0858d3bab51ba7e386637f22a61d8ccddaeec5f3fe4209da3a6168dbb91573e0"}, 123 | ] 124 | 125 | [[package]] 126 | name = "httpx-ws" 127 | version = "0.6.2" 128 | requires_python = ">=3.8" 129 | summary = "WebSockets support for HTTPX" 130 | groups = ["default"] 131 | dependencies = [ 132 | "anyio>=4", 133 | "httpcore>=1.0.4", 134 | "httpx>=0.23.1", 135 | "wsproto", 136 | ] 137 | files = [ 138 | {file = "httpx_ws-0.6.2-py3-none-any.whl", hash = "sha256:24f87427acb757ada200aeab016cc429fa0bc71b0730429c37634867194e305c"}, 139 | {file = "httpx_ws-0.6.2.tar.gz", hash = "sha256:b07446b9067a30f1012fa9851fdfd14207012cd657c485565884f90553d0854c"}, 140 | ] 141 | 142 | [[package]] 143 | name = "idna" 144 | version = "3.10" 145 | requires_python = ">=3.6" 146 | summary = "Internationalized Domain Names in Applications (IDNA)" 147 | groups = ["default"] 148 | files = [ 149 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 150 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 151 | ] 152 | 153 | [[package]] 154 | name = "iniconfig" 155 | version = "2.0.0" 156 | requires_python = ">=3.7" 157 | summary = "brain-dead simple config-ini parsing" 158 | groups = ["dev"] 159 | files = [ 160 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 161 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 162 | ] 163 | 164 | [[package]] 165 | name = "ormsgpack" 166 | version = "1.6.0" 167 | requires_python = ">=3.8" 168 | summary = "Fast, correct Python msgpack library supporting dataclasses, datetimes, and numpy" 169 | groups = ["default"] 170 | files = [ 171 | {file = "ormsgpack-1.6.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:613e851edde1dc3b94e84e9d8046b9c603fce1e5bf40f3ebe129a81f9a31d4b9"}, 172 | {file = "ormsgpack-1.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8e2b858cc378d2c46161bebfe6232de1e158eb2e7dfdf07172fe183bf8a9333"}, 173 | {file = "ormsgpack-1.6.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4003867208e3b9b9471c0a4bbf68479dcde69137ca1c5860bd7236410bf65024"}, 174 | {file = "ormsgpack-1.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fd584dd40d479a60cbae0ca59792b82144f35e355c363771566bb7d337ba8a9"}, 175 | {file = "ormsgpack-1.6.0-cp310-none-win_amd64.whl", hash = "sha256:e8b2cab0ddb98b1b26f01da552a76138299ecf29f8a04fe34ac5b686b9db02d0"}, 176 | {file = "ormsgpack-1.6.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6f08a3c448e9ca65c7b5af600b3d795ffbee0dbd5b4defc6e12f89d2d56c57b4"}, 177 | {file = "ormsgpack-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:978330de00c1d8c20b62ae38f20b85312abefb6e46342617ba47d9a01b70d727"}, 178 | {file = "ormsgpack-1.6.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6b747ae7f2fa8e54cf7033a9907cc2dfeee70184148ce57d1c8dff3ed2d3692"}, 179 | {file = "ormsgpack-1.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:243b330577c8158a89fec4abce40254cf907637ea7e604bf866b3e39b3c5a819"}, 180 | {file = "ormsgpack-1.6.0-cp311-none-win_amd64.whl", hash = "sha256:75e1837f569fb6ae2f0c6415e983a0300b633b69a5d3480f34fb0a61f1662dbf"}, 181 | {file = "ormsgpack-1.6.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:af017e8dcf8dad7b36c018e2ec410eb2bedd123aa81beabc808b92f00a76c285"}, 182 | {file = "ormsgpack-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c42c72050499d57d620ae81bf3b47637fb66b1c90a8abcabc0fbf9b193fdabc"}, 183 | {file = "ormsgpack-1.6.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:472a6e9207caad84ad3bddd6c6be54e99fdb7ef667cedc1074f26386aebd6580"}, 184 | {file = "ormsgpack-1.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33075e026d8536c6953d0485c34970f62dcc309d662f5913147bfbebe8b1d59f"}, 185 | {file = "ormsgpack-1.6.0-cp312-none-win_amd64.whl", hash = "sha256:f28b4a0cd7f64b92e4a0de6acbd7d03d20970fdded9e752fa6dedb3b95be86a3"}, 186 | {file = "ormsgpack-1.6.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:113316087520ec70f7c0f36450181ca24d575d4225116545c9038d6e8e576f51"}, 187 | {file = "ormsgpack-1.6.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a72971eeccfc2e720491abae3fc7f368886d81b7ef3f965eb2adb7922454243"}, 188 | {file = "ormsgpack-1.6.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e17d9b9b0a61ef3f26a66c85d9b3140a2111e4f2c5b4daf04185eac872b39303"}, 189 | {file = "ormsgpack-1.6.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9219d871d05370443129679241a6c4bd579ff351a2c7675ff2bb192485f543e6"}, 190 | {file = "ormsgpack-1.6.0-cp313-none-win_amd64.whl", hash = "sha256:a0ee8ae2981548df90a4ff0a8cde710cd289a19980e74a338d6c958a4e6c1c84"}, 191 | {file = "ormsgpack-1.6.0.tar.gz", hash = "sha256:0c9612147f3c406b56eba6a576948057ada711bda0831f192afd46be6e5dd91e"}, 192 | ] 193 | 194 | [[package]] 195 | name = "packaging" 196 | version = "24.2" 197 | requires_python = ">=3.8" 198 | summary = "Core utilities for Python packages" 199 | groups = ["dev"] 200 | files = [ 201 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 202 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 203 | ] 204 | 205 | [[package]] 206 | name = "pluggy" 207 | version = "1.5.0" 208 | requires_python = ">=3.8" 209 | summary = "plugin and hook calling mechanisms for python" 210 | groups = ["dev"] 211 | files = [ 212 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 213 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 214 | ] 215 | 216 | [[package]] 217 | name = "pydantic" 218 | version = "2.10.3" 219 | requires_python = ">=3.8" 220 | summary = "Data validation using Python type hints" 221 | groups = ["default"] 222 | dependencies = [ 223 | "annotated-types>=0.6.0", 224 | "pydantic-core==2.27.1", 225 | "typing-extensions>=4.12.2", 226 | ] 227 | files = [ 228 | {file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"}, 229 | {file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"}, 230 | ] 231 | 232 | [[package]] 233 | name = "pydantic-core" 234 | version = "2.27.1" 235 | requires_python = ">=3.8" 236 | summary = "Core functionality for Pydantic validation and serialization" 237 | groups = ["default"] 238 | dependencies = [ 239 | "typing-extensions!=4.7.0,>=4.6.0", 240 | ] 241 | files = [ 242 | {file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"}, 243 | {file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"}, 244 | {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"}, 245 | {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"}, 246 | {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"}, 247 | {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"}, 248 | {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"}, 249 | {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"}, 250 | {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"}, 251 | {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"}, 252 | {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"}, 253 | {file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"}, 254 | {file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"}, 255 | {file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"}, 256 | {file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"}, 257 | {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"}, 258 | {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"}, 259 | {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"}, 260 | {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"}, 261 | {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"}, 262 | {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"}, 263 | {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"}, 264 | {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"}, 265 | {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"}, 266 | {file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"}, 267 | {file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"}, 268 | {file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"}, 269 | {file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"}, 270 | {file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"}, 271 | {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"}, 272 | {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"}, 273 | {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"}, 274 | {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"}, 275 | {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"}, 276 | {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"}, 277 | {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"}, 278 | {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"}, 279 | {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"}, 280 | {file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"}, 281 | {file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"}, 282 | {file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"}, 283 | {file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"}, 284 | {file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"}, 285 | {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"}, 286 | {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"}, 287 | {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"}, 288 | {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"}, 289 | {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"}, 290 | {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"}, 291 | {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"}, 292 | {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"}, 293 | {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"}, 294 | {file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"}, 295 | {file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"}, 296 | {file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"}, 297 | {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"}, 298 | {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"}, 299 | {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"}, 300 | {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"}, 301 | {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"}, 302 | {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"}, 303 | {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"}, 304 | {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"}, 305 | {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"}, 306 | {file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"}, 307 | ] 308 | 309 | [[package]] 310 | name = "pytest" 311 | version = "8.3.4" 312 | requires_python = ">=3.8" 313 | summary = "pytest: simple powerful testing with Python" 314 | groups = ["dev"] 315 | dependencies = [ 316 | "colorama; sys_platform == \"win32\"", 317 | "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", 318 | "iniconfig", 319 | "packaging", 320 | "pluggy<2,>=1.5", 321 | "tomli>=1; python_version < \"3.11\"", 322 | ] 323 | files = [ 324 | {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, 325 | {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, 326 | ] 327 | 328 | [[package]] 329 | name = "pytest-asyncio" 330 | version = "0.24.0" 331 | requires_python = ">=3.8" 332 | summary = "Pytest support for asyncio" 333 | groups = ["dev"] 334 | dependencies = [ 335 | "pytest<9,>=8.2", 336 | ] 337 | files = [ 338 | {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, 339 | {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, 340 | ] 341 | 342 | [[package]] 343 | name = "python-dotenv" 344 | version = "1.0.1" 345 | requires_python = ">=3.8" 346 | summary = "Read key-value pairs from a .env file and set them as environment variables" 347 | groups = ["dev"] 348 | files = [ 349 | {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, 350 | {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, 351 | ] 352 | 353 | [[package]] 354 | name = "sniffio" 355 | version = "1.3.1" 356 | requires_python = ">=3.7" 357 | summary = "Sniff out which async library your code is running under" 358 | groups = ["default"] 359 | files = [ 360 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 361 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 362 | ] 363 | 364 | [[package]] 365 | name = "tomli" 366 | version = "2.2.1" 367 | requires_python = ">=3.8" 368 | summary = "A lil' TOML parser" 369 | groups = ["dev"] 370 | marker = "python_version < \"3.11\"" 371 | files = [ 372 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 373 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 374 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 375 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 376 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 377 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 378 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 379 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 380 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 381 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 382 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 383 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 384 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 385 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 386 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 387 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 388 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 389 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 390 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 391 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 392 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 393 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 394 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 395 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 396 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 397 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 398 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 399 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 400 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 401 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 402 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 403 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 404 | ] 405 | 406 | [[package]] 407 | name = "typing-extensions" 408 | version = "4.12.2" 409 | requires_python = ">=3.8" 410 | summary = "Backported and Experimental Type Hints for Python 3.8+" 411 | groups = ["default"] 412 | files = [ 413 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 414 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 415 | ] 416 | 417 | [[package]] 418 | name = "wsproto" 419 | version = "1.2.0" 420 | requires_python = ">=3.7.0" 421 | summary = "WebSockets state-machine based protocol implementation" 422 | groups = ["default"] 423 | dependencies = [ 424 | "h11<1,>=0.9.0", 425 | ] 426 | files = [ 427 | {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, 428 | {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, 429 | ] 430 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "fish-audio-sdk" 3 | version = "2025.06.03" 4 | description = "fish.audio platform api sdk" 5 | authors = [ 6 | {name = "abersheeran", email = "me@abersheeran.com"}, 7 | ] 8 | dependencies = [ 9 | "httpx>=0.27.2", 10 | "ormsgpack>=1.5.0", 11 | "pydantic>=2.9.1", 12 | "httpx-ws>=0.6.2", 13 | ] 14 | requires-python = ">=3.10" 15 | readme = "README.md" 16 | license = {text = "MIT"} 17 | 18 | [build-system] 19 | requires = ["pdm-backend"] 20 | build-backend = "pdm.backend" 21 | 22 | [tool.pdm] 23 | package-type = "library" 24 | 25 | [tool.pdm.dev-dependencies] 26 | dev = [ 27 | "pytest>=8.3.3", 28 | "pytest-asyncio>=0.24.0", 29 | "python-dotenv>=1.0.1", 30 | ] 31 | 32 | [tool.pytest.ini_options] 33 | asyncio_mode = "auto" 34 | -------------------------------------------------------------------------------- /src/fish_audio_sdk/__init__.py: -------------------------------------------------------------------------------- 1 | from .apis import Session 2 | from .exceptions import HttpCodeErr 3 | from .schemas import ASRRequest, TTSRequest, ReferenceAudio, Prosody, PaginatedResponse, ModelEntity, APICreditEntity, StartEvent, TextEvent, CloseEvent 4 | from .websocket import WebSocketSession, AsyncWebSocketSession 5 | 6 | __all__ = [ 7 | "Session", 8 | "HttpCodeErr", 9 | "ReferenceAudio", 10 | "TTSRequest", 11 | "ASRRequest", 12 | "WebSocketSession", 13 | "AsyncWebSocketSession", 14 | "Prosody", 15 | "PaginatedResponse", 16 | "ModelEntity", 17 | "APICreditEntity", 18 | "StartEvent", 19 | "TextEvent", 20 | "CloseEvent", 21 | ] 22 | -------------------------------------------------------------------------------- /src/fish_audio_sdk/apis.py: -------------------------------------------------------------------------------- 1 | from typing import Generator, Literal 2 | 3 | import ormsgpack 4 | 5 | from .io import G, GStream, RemoteCall, Request, convert, convert_stream 6 | from .schemas import ( 7 | APICreditEntity, 8 | ASRRequest, 9 | ASRResponse, 10 | ModelEntity, 11 | Backends, 12 | PackageEntity, 13 | PaginatedResponse, 14 | TTSRequest, 15 | ) 16 | 17 | 18 | class Session(RemoteCall): 19 | @convert_stream 20 | def tts(self, request: TTSRequest, backend: Backends = "speech-1.5") -> GStream: 21 | yield Request( 22 | method="POST", 23 | url="/v1/tts", 24 | headers={"Content-Type": "application/msgpack", "model": backend}, 25 | content=ormsgpack.packb(request.model_dump()), 26 | ) 27 | 28 | def g() -> Generator[bytes, bytes, None]: 29 | chunk = yield b"" 30 | while True: 31 | chunk = yield chunk 32 | if chunk == b"": 33 | break 34 | 35 | return g() 36 | 37 | @convert 38 | def asr(self, request: ASRRequest) -> G[ASRResponse]: 39 | response = yield Request( 40 | method="POST", 41 | url="/v1/asr", 42 | headers={"Content-Type": "application/msgpack"}, 43 | content=ormsgpack.packb(request.model_dump()), 44 | ) 45 | return ASRResponse.model_validate(response.json()) 46 | 47 | @convert 48 | def list_models( 49 | this, 50 | *, 51 | page_size: int = 10, 52 | page_number: int = 1, 53 | title: str | None = None, 54 | tag: list[str] | str | None = None, 55 | self_only: bool = False, 56 | author_id: str | None = None, 57 | language: list[str] | str | None = None, 58 | title_language: list[str] | str | None = None, 59 | sort_by: Literal["task_count", "created_at"] = "task_count", 60 | ) -> G[PaginatedResponse[ModelEntity]]: 61 | response = yield Request( 62 | method="GET", 63 | url="/model", 64 | params=filter_none( 65 | { 66 | "page_size": page_size, 67 | "page_number": page_number, 68 | "title": title, 69 | "tag": tag, 70 | "self": self_only, 71 | "author_id": author_id, 72 | "language": language, 73 | "title_language": title_language, 74 | "sort_by": sort_by, 75 | } 76 | ), 77 | ) 78 | return PaginatedResponse[ModelEntity].model_validate(response.json()) 79 | 80 | @convert 81 | def get_model(this, model_id: str) -> G[ModelEntity]: 82 | response = yield Request(method="GET", url=f"/model/{model_id}") 83 | return ModelEntity.model_validate(response.json()) 84 | 85 | @convert 86 | def create_model( 87 | this, 88 | *, 89 | visibility: Literal["public", "unlist", "private"] = "private", 90 | type: Literal["tts"] = "tts", 91 | title: str, 92 | description: str | None = None, 93 | cover_image: bytes | None = None, 94 | train_mode: Literal["fast"] = "fast", 95 | voices: list[bytes], 96 | texts: list[str] | None = None, 97 | tags: list[str] | None = None, 98 | enhance_audio_quality: bool = True, 99 | ) -> G[ModelEntity]: 100 | if texts is None: 101 | texts = [] 102 | 103 | if tags is None: 104 | tags = [] 105 | 106 | files = [("voices", voice) for voice in voices] 107 | if cover_image is not None: 108 | files.append(("cover_image", cover_image)) 109 | response = yield Request( 110 | method="POST", 111 | url="/model", 112 | data=filter_none( 113 | { 114 | "visibility": visibility, 115 | "type": type, 116 | "title": title, 117 | "description": description, 118 | "train_mode": train_mode, 119 | "texts": texts, 120 | "tags": tags, 121 | "enhance_audio_quality": enhance_audio_quality, 122 | } 123 | ), 124 | files=files, 125 | ) 126 | return ModelEntity.model_validate(response.json()) 127 | 128 | @convert 129 | def delete_model(this, model_id: str) -> G[None]: 130 | yield Request(method="DELETE", url=f"/model/{model_id}") 131 | 132 | @convert 133 | def update_model( 134 | this, 135 | model_id: str, 136 | *, 137 | title: str | None = None, 138 | description: str | None = None, 139 | cover_image: bytes | None = None, 140 | visibility: Literal["public", "unlist", "private"] | None = None, 141 | tags: list[str] | None = None, 142 | ) -> G[None]: 143 | files = [] 144 | if cover_image is not None: 145 | files.append(("cover_image", cover_image)) 146 | yield Request( 147 | method="PATCH", 148 | url=f"/model/{model_id}", 149 | data=filter_none( 150 | { 151 | "title": title, 152 | "description": description, 153 | "visibility": visibility, 154 | "tags": tags, 155 | } 156 | ), 157 | files=files, 158 | ) 159 | 160 | @convert 161 | def get_api_credit(this) -> G[APICreditEntity]: 162 | response = yield Request(method="GET", url="/wallet/self/api-credit") 163 | return APICreditEntity.model_validate(response.json()) 164 | 165 | @convert 166 | def get_package(this) -> G[PackageEntity]: 167 | response = yield Request(method="GET", url="/wallet/self/package") 168 | return PackageEntity.model_validate(response.json()) 169 | 170 | 171 | filter_none = lambda d: {k: v for k, v in d.items() if v is not None} 172 | -------------------------------------------------------------------------------- /src/fish_audio_sdk/exceptions.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass(init=False) 5 | class HttpCodeErr(Exception): 6 | status: int 7 | message: str 8 | 9 | def __init__(self, status: int, message: str): 10 | self.status = status 11 | self.message = message 12 | super().__init__(f"{status} {message}") 13 | 14 | 15 | class WebSocketErr(Exception): 16 | """ 17 | {"event": "finish", "reason": "error"} or WebSocketDisconnect 18 | """ 19 | -------------------------------------------------------------------------------- /src/fish_audio_sdk/io.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | import typing 3 | from http.client import responses as http_responses 4 | from typing import ( 5 | Any, 6 | AsyncGenerator, 7 | Awaitable, 8 | Callable, 9 | Concatenate, 10 | Generator, 11 | Generic, 12 | ParamSpec, 13 | TypeVar, 14 | ) 15 | 16 | import httpx 17 | import httpx._client 18 | import httpx._types 19 | 20 | from .exceptions import HttpCodeErr 21 | 22 | 23 | class RemoteCall: 24 | _base_url: str 25 | _async_client: httpx.AsyncClient 26 | _sync_client: httpx.Client 27 | 28 | def __init__(self, apikey: str, *, base_url: str = "https://api.fish.audio"): 29 | self._apikey = apikey 30 | self._base_url = base_url 31 | self.init_async_client() 32 | self.init_sync_client() 33 | 34 | def init_async_client(self): 35 | self._async_client = httpx.AsyncClient( 36 | base_url=self._base_url, 37 | headers={"Authorization": f"Bearer {self._apikey}"}, 38 | timeout=None, 39 | ) 40 | 41 | def init_sync_client(self): 42 | self._sync_client = httpx.Client( 43 | base_url=self._base_url, 44 | headers={"Authorization": f"Bearer {self._apikey}"}, 45 | timeout=None, 46 | ) 47 | 48 | async def __aenter__(self): 49 | if self._async_client.is_closed: 50 | self.init_async_client() 51 | return self 52 | 53 | async def __aexit__(self, exc_type, exc_value, traceback): 54 | await self._async_client.aclose() 55 | 56 | def __enter__(self): 57 | if self._sync_client.is_closed: 58 | self.init_sync_client() 59 | return self 60 | 61 | def __exit__(self, exc_type, exc_value, traceback): 62 | self._sync_client.close() 63 | 64 | @staticmethod 65 | def _try_raise_http_exception(resp: httpx.Response) -> None: 66 | if not resp.is_success: 67 | try: 68 | raise HttpCodeErr(**resp.json()) 69 | except httpx.ResponseNotRead: 70 | raise HttpCodeErr( 71 | status=resp.status_code, message=http_responses[resp.status_code] 72 | ) 73 | except TypeError: 74 | raise HttpCodeErr( 75 | status=resp.status_code, message=resp.json()["detail"] 76 | ) 77 | 78 | 79 | P = ParamSpec("P") 80 | R = TypeVar("R") 81 | 82 | 83 | @dataclasses.dataclass 84 | class IOCall(Generic[P, R]): 85 | _awaitable: Callable[Concatenate[RemoteCall, P], Awaitable[R]] 86 | _syncable: Callable[Concatenate[RemoteCall, P], R] 87 | this: RemoteCall 88 | 89 | def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: 90 | return self._syncable(self.this, *args, **kwargs) 91 | 92 | def awaitable(self, *args: P.args, **kwargs: P.kwargs) -> Awaitable[R]: 93 | return self._awaitable(self.this, *args, **kwargs) 94 | 95 | 96 | class IOCallDescriptor(Generic[P, R]): 97 | def __init__( 98 | self, 99 | awaitable: Callable[Concatenate[RemoteCall, P], Awaitable[R]], 100 | syncable: Callable[Concatenate[RemoteCall, P], R], 101 | ): 102 | self.awaitable = awaitable 103 | self.syncable = syncable 104 | 105 | def __get__(self, instance: RemoteCall, owner: type[RemoteCall]) -> IOCall[P, R]: 106 | return IOCall(self.awaitable, self.syncable, instance) 107 | 108 | 109 | @dataclasses.dataclass 110 | class StreamIOCall(Generic[P]): 111 | _awaitable: Callable[Concatenate[RemoteCall, P], AsyncGenerator[bytes, None]] 112 | _syncable: Callable[Concatenate[RemoteCall, P], Generator[bytes, None, None]] 113 | this: RemoteCall 114 | 115 | def __call__( 116 | self, *args: P.args, **kwargs: P.kwargs 117 | ) -> Generator[bytes, None, None]: 118 | return self._syncable(self.this, *args, **kwargs) 119 | 120 | async def awaitable( 121 | self, *args: P.args, **kwargs: P.kwargs 122 | ) -> AsyncGenerator[bytes, None]: 123 | async for chunk in self._awaitable(self.this, *args, **kwargs): 124 | yield chunk 125 | 126 | 127 | class StreamIOCallDescriptor(Generic[P]): 128 | def __init__( 129 | self, 130 | awaitable: Callable[Concatenate[RemoteCall, P], AsyncGenerator[bytes, None]], 131 | syncable: Callable[Concatenate[RemoteCall, P], Generator[bytes, None, None]], 132 | ): 133 | self.awaitable = awaitable 134 | self.syncable = syncable 135 | 136 | def __get__(self, instance: RemoteCall, owner: type[RemoteCall]) -> StreamIOCall[P]: 137 | return StreamIOCall(self.awaitable, self.syncable, instance) 138 | 139 | 140 | @dataclasses.dataclass 141 | class Request: 142 | method: str 143 | url: str 144 | 145 | content: httpx._types.RequestContent | None = None 146 | data: httpx._types.RequestData | None = None 147 | files: httpx._types.RequestFiles | None = None 148 | json: Any | None = None 149 | params: httpx._types.QueryParamTypes | None = None 150 | headers: httpx._types.HeaderTypes | None = None 151 | cookies: httpx._types.CookieTypes | None = None 152 | timeout: httpx._types.TimeoutTypes = None 153 | extensions: httpx._types.RequestExtensions | None = None 154 | 155 | 156 | Response = httpx.Response 157 | 158 | 159 | G = Generator[Request, Response, R] 160 | 161 | 162 | def convert( 163 | func: Callable[Concatenate[typing.Any, P], Generator[Request, Response, R]], 164 | ) -> IOCallDescriptor[P, R]: 165 | async def async_wrapper(self: RemoteCall, *args: P.args, **kwargs: P.kwargs) -> R: 166 | g = func(self, *args, **kwargs) 167 | request = next(g) 168 | 169 | request = self._async_client.build_request(**dataclasses.asdict(request)) 170 | resp = await self._async_client.send(request) 171 | self._try_raise_http_exception(resp) 172 | try: 173 | g.send(resp) 174 | except StopIteration as exc: 175 | return exc.value 176 | raise RuntimeError("Generator did not stop") 177 | 178 | def sync_wrapper(self: RemoteCall, *args: P.args, **kwargs: P.kwargs) -> R: 179 | g = func(self, *args, **kwargs) 180 | request = next(g) 181 | 182 | request = self._sync_client.build_request(**dataclasses.asdict(request)) 183 | resp = self._sync_client.send(request) 184 | self._try_raise_http_exception(resp) 185 | try: 186 | g.send(resp) 187 | except StopIteration as exc: 188 | return exc.value 189 | raise RuntimeError("Generator did not stop") 190 | 191 | call = IOCallDescriptor(async_wrapper, sync_wrapper) 192 | return call 193 | 194 | 195 | GStream = G[Generator[bytes, bytes, None]] 196 | 197 | 198 | def convert_stream( 199 | func: Callable[ 200 | Concatenate[typing.Any, P], 201 | Generator[Request, Response, Generator[bytes, bytes, None]], 202 | ], 203 | ) -> StreamIOCallDescriptor[P]: 204 | async def async_wrapper( 205 | self: RemoteCall, *args: P.args, **kwargs: P.kwargs 206 | ) -> AsyncGenerator[bytes, None]: 207 | g = func(self, *args, **kwargs) 208 | request = next(g) 209 | 210 | request = self._async_client.build_request(**dataclasses.asdict(request)) 211 | resp = await self._async_client.send(request, stream=True) 212 | self._try_raise_http_exception(resp) 213 | try: 214 | g.send(resp) 215 | except StopIteration as exc: 216 | generator: Generator[bytes, bytes, None] = exc.value 217 | next(generator) 218 | try: 219 | async for chunk in resp.aiter_bytes(): 220 | yield generator.send(chunk) 221 | yield generator.send(b"") 222 | except StopIteration: 223 | return 224 | finally: 225 | generator.close() 226 | 227 | raise RuntimeError("Generator did not stop") 228 | 229 | def sync_wrapper( 230 | self: RemoteCall, *args: P.args, **kwargs: P.kwargs 231 | ) -> Generator[bytes, None, None]: 232 | g = func(self, *args, **kwargs) 233 | request = next(g) 234 | 235 | request = self._sync_client.build_request(**dataclasses.asdict(request)) 236 | resp = self._sync_client.send(request, stream=True) 237 | self._try_raise_http_exception(resp) 238 | try: 239 | g.send(resp) 240 | except StopIteration as exc: 241 | try: 242 | generator: Generator[bytes, bytes, None] = exc.value 243 | next(generator) 244 | for chunk in resp.iter_bytes(): 245 | yield generator.send(chunk) 246 | yield generator.send(b"") 247 | except StopIteration: 248 | return 249 | finally: 250 | generator.close() 251 | 252 | raise RuntimeError("Generator did not stop") 253 | 254 | call = StreamIOCallDescriptor(async_wrapper, sync_wrapper) 255 | return call 256 | -------------------------------------------------------------------------------- /src/fish_audio_sdk/schemas.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import decimal 3 | from typing import Annotated, Generic, Literal, TypeVar 4 | 5 | from pydantic import BaseModel, Field 6 | 7 | 8 | Backends = Literal["speech-1.5", "speech-1.6", "agent-x0", "s1", "s1-mini"] 9 | 10 | Item = TypeVar("Item") 11 | 12 | 13 | class PaginatedResponse(BaseModel, Generic[Item]): 14 | total: int 15 | items: list[Item] 16 | 17 | 18 | class ReferenceAudio(BaseModel): 19 | audio: bytes 20 | text: str 21 | 22 | 23 | class Prosody(BaseModel): 24 | speed: float = 1.0 25 | volume: float = 0.0 26 | 27 | 28 | class TTSRequest(BaseModel): 29 | text: str 30 | chunk_length: Annotated[int, Field(ge=100, le=300, strict=True)] = 200 31 | format: Literal["wav", "pcm", "mp3"] = "mp3" 32 | sample_rate: int | None = None 33 | mp3_bitrate: Literal[64, 128, 192] = 128 34 | opus_bitrate: Literal[-1000, 24, 32, 48, 64] = 32 35 | references: list[ReferenceAudio] = [] 36 | reference_id: str | None = None 37 | normalize: bool = True 38 | latency: Literal["normal", "balanced"] = "balanced" 39 | prosody: Prosody | None = None 40 | top_p: float = 0.7 41 | temperature: float = 0.7 42 | 43 | 44 | class ASRRequest(BaseModel): 45 | audio: bytes 46 | language: str | None = None 47 | ignore_timestamps: bool | None = None 48 | 49 | 50 | class ASRSegment(BaseModel): 51 | text: str 52 | start: float 53 | end: float 54 | 55 | 56 | class ASRResponse(BaseModel): 57 | text: str 58 | # Duration in milliseconds 59 | duration: float 60 | segments: list[ASRSegment] 61 | 62 | 63 | class SampleEntity(BaseModel): 64 | title: str 65 | text: str 66 | task_id: str 67 | audio: str 68 | 69 | 70 | class AuthorEntity(BaseModel): 71 | id: str = Field(alias="_id") 72 | nickname: str 73 | avatar: str 74 | 75 | 76 | class ModelEntity(BaseModel): 77 | id: str = Field(alias="_id") 78 | type: Literal["svc", "tts"] 79 | title: str 80 | description: str 81 | cover_image: str 82 | train_mode: Literal["fast", "full"] 83 | state: Literal["created", "training", "trained", "failed"] 84 | tags: list[str] 85 | samples: list[SampleEntity] 86 | created_at: datetime.datetime 87 | updated_at: datetime.datetime 88 | languages: list[str] 89 | visibility: Literal["public", "unlist", "private"] 90 | lock_visibility: bool 91 | 92 | like_count: int 93 | mark_count: int 94 | shared_count: int 95 | task_count: int 96 | 97 | liked: bool = False 98 | marked: bool = False 99 | 100 | author: AuthorEntity 101 | 102 | 103 | class APICreditEntity(BaseModel): 104 | _id: str 105 | user_id: str 106 | credit: decimal.Decimal 107 | created_at: str 108 | updated_at: str 109 | 110 | 111 | class PackageEntity(BaseModel): 112 | _id: str 113 | user_id: str 114 | type: str 115 | total: int 116 | balance: int 117 | created_at: str 118 | updated_at: str 119 | finished_at: str 120 | 121 | 122 | class StartEvent(BaseModel): 123 | event: Literal["start"] = "start" 124 | request: TTSRequest 125 | 126 | 127 | class TextEvent(BaseModel): 128 | event: Literal["text"] = "text" 129 | text: str 130 | 131 | 132 | class CloseEvent(BaseModel): 133 | event: Literal["stop"] = "stop" 134 | -------------------------------------------------------------------------------- /src/fish_audio_sdk/websocket.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from concurrent.futures import ThreadPoolExecutor 3 | from typing import AsyncGenerator, AsyncIterable, Generator, Iterable 4 | 5 | import httpx 6 | import ormsgpack 7 | from httpx_ws import WebSocketDisconnect, connect_ws, aconnect_ws 8 | 9 | from .exceptions import WebSocketErr 10 | 11 | from .schemas import Backends, CloseEvent, StartEvent, TTSRequest, TextEvent 12 | 13 | 14 | class WebSocketSession: 15 | def __init__( 16 | self, 17 | apikey: str, 18 | *, 19 | base_url: str = "https://api.fish.audio", 20 | max_workers: int = 10, 21 | ): 22 | self._apikey = apikey 23 | self._base_url = base_url 24 | self._executor = ThreadPoolExecutor(max_workers=max_workers) 25 | self._client = httpx.Client( 26 | base_url=self._base_url, 27 | headers={"Authorization": f"Bearer {self._apikey}"}, 28 | ) 29 | 30 | def __enter__(self): 31 | return self 32 | 33 | def __exit__(self, exc_type, exc_value, traceback): 34 | self.close() 35 | 36 | def close(self): 37 | self._client.close() 38 | 39 | def tts( 40 | self, 41 | request: TTSRequest, 42 | text_stream: Iterable[str], 43 | backend: Backends = "speech-1.5", 44 | ) -> Generator[bytes, None, None]: 45 | with connect_ws( 46 | "/v1/tts/live", 47 | client=self._client, 48 | headers={"model": backend}, 49 | ) as ws: 50 | 51 | def sender(): 52 | ws.send_bytes( 53 | ormsgpack.packb( 54 | StartEvent(request=request).model_dump(), 55 | ) 56 | ) 57 | for text in text_stream: 58 | ws.send_bytes( 59 | ormsgpack.packb( 60 | TextEvent(text=text).model_dump(), 61 | ) 62 | ) 63 | ws.send_bytes( 64 | ormsgpack.packb( 65 | CloseEvent().model_dump(), 66 | ) 67 | ) 68 | 69 | sender_future = self._executor.submit(sender) 70 | 71 | while True: 72 | try: 73 | message = ws.receive_bytes() 74 | data = ormsgpack.unpackb(message) 75 | match data["event"]: 76 | case "audio": 77 | yield data["audio"] 78 | case "finish" if data["reason"] == "error": 79 | raise WebSocketErr 80 | case "finish" if data["reason"] == "stop": 81 | break 82 | except WebSocketDisconnect: 83 | raise WebSocketErr 84 | 85 | sender_future.result() 86 | 87 | 88 | class AsyncWebSocketSession: 89 | def __init__( 90 | self, 91 | apikey: str, 92 | *, 93 | base_url: str = "https://api.fish.audio", 94 | ): 95 | self._apikey = apikey 96 | self._base_url = base_url 97 | self._client = httpx.AsyncClient( 98 | base_url=self._base_url, 99 | headers={"Authorization": f"Bearer {self._apikey}"}, 100 | ) 101 | 102 | async def __aenter__(self): 103 | return self 104 | 105 | async def __aexit__(self, exc_type, exc_value, traceback): 106 | await self.close() 107 | 108 | async def close(self): 109 | await self._client.aclose() 110 | 111 | async def tts( 112 | self, 113 | request: TTSRequest, 114 | text_stream: AsyncIterable[str], 115 | backend: Backends = "speech-1.5", 116 | ) -> AsyncGenerator[bytes, None]: 117 | async with aconnect_ws( 118 | "/v1/tts/live", 119 | client=self._client, 120 | headers={"model": backend}, 121 | ) as ws: 122 | 123 | async def sender(): 124 | await ws.send_bytes( 125 | ormsgpack.packb( 126 | StartEvent(request=request).model_dump(), 127 | ) 128 | ) 129 | async for text in text_stream: 130 | await ws.send_bytes( 131 | ormsgpack.packb( 132 | TextEvent(text=text).model_dump(), 133 | ) 134 | ) 135 | await ws.send_bytes( 136 | ormsgpack.packb( 137 | CloseEvent().model_dump(), 138 | ) 139 | ) 140 | 141 | sender_future = asyncio.get_running_loop().create_task(sender()) 142 | 143 | while True: 144 | try: 145 | message = await ws.receive_bytes() 146 | data = ormsgpack.unpackb(message) 147 | match data["event"]: 148 | case "audio": 149 | yield data["audio"] 150 | case "finish" if data["reason"] == "error": 151 | raise WebSocketErr 152 | case "finish" if data["reason"] == "stop": 153 | break 154 | except WebSocketDisconnect: 155 | raise WebSocketErr 156 | 157 | await sender_future 158 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import dotenv 2 | 3 | dotenv.load_dotenv() 4 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from fish_audio_sdk import Session, WebSocketSession, AsyncWebSocketSession 6 | 7 | APIKEY = os.environ["APIKEY"] 8 | 9 | 10 | @pytest.fixture 11 | def session(): 12 | return Session(APIKEY) 13 | 14 | 15 | @pytest.fixture 16 | def sync_websocket(): 17 | return WebSocketSession(APIKEY) 18 | 19 | 20 | @pytest.fixture 21 | def async_websocket(): 22 | return AsyncWebSocketSession(APIKEY) 23 | -------------------------------------------------------------------------------- /tests/hello.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fishaudio/fish-audio-python/9aef0b75e0f45ab74e8fa402f191725576b26a80/tests/hello.mp3 -------------------------------------------------------------------------------- /tests/test_apis.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from fish_audio_sdk import ASRRequest, HttpCodeErr, Session, TTSRequest 4 | from fish_audio_sdk.schemas import APICreditEntity, PackageEntity 5 | 6 | 7 | def test_tts(session: Session): 8 | buffer = bytearray() 9 | for chunk in session.tts(TTSRequest(text="Hello, world!")): 10 | buffer.extend(chunk) 11 | assert len(buffer) > 0 12 | 13 | def test_tts_model_1_6(session: Session): 14 | buffer = bytearray() 15 | for chunk in session.tts(TTSRequest(text="Hello, world!"), backend="speech-1.6"): 16 | buffer.extend(chunk) 17 | assert len(buffer) > 0 18 | 19 | 20 | async def test_tts_async(session: Session): 21 | buffer = bytearray() 22 | async for chunk in session.tts.awaitable(TTSRequest(text="Hello, world!")): 23 | buffer.extend(chunk) 24 | assert len(buffer) > 0 25 | 26 | 27 | def test_asr(session: Session): 28 | buffer = bytearray() 29 | for chunk in session.tts(TTSRequest(text="Hello, world!")): 30 | buffer.extend(chunk) 31 | res = session.asr(ASRRequest(audio=buffer, language="zh")) 32 | assert res.text 33 | 34 | 35 | async def test_asr_async(session: Session): 36 | buffer = bytearray() 37 | async for chunk in session.tts.awaitable(TTSRequest(text="Hello, world!")): 38 | buffer.extend(chunk) 39 | res = await session.asr.awaitable(ASRRequest(audio=buffer, language="zh")) 40 | assert res.text 41 | 42 | 43 | def test_list_models(session: Session): 44 | res = session.list_models() 45 | assert res.total > 0 46 | 47 | 48 | async def test_list_models_async(session: Session): 49 | res = await session.list_models.awaitable() 50 | assert res.total > 0 51 | 52 | 53 | def test_list_self_models(session: Session): 54 | res = session.list_models(self_only=True) 55 | assert res.total > 0 56 | 57 | 58 | def test_get_model(session: Session): 59 | res = session.get_model(model_id="7f92f8afb8ec43bf81429cc1c9199cb1") 60 | assert res.id == "7f92f8afb8ec43bf81429cc1c9199cb1" 61 | 62 | 63 | def test_get_model_not_found(session: Session): 64 | with pytest.raises(HttpCodeErr) as exc_info: 65 | session.get_model(model_id="123") 66 | assert exc_info.value.status == 404 67 | 68 | 69 | def test_invalid_token(session: Session): 70 | session._apikey = "invalid" 71 | session.init_async_client() 72 | session.init_sync_client() 73 | 74 | with pytest.raises(HttpCodeErr) as exc_info: 75 | test_tts(session) 76 | 77 | assert exc_info.value.status in [401, 402] 78 | 79 | 80 | def test_get_api_credit(session: Session): 81 | res = session.get_api_credit() 82 | assert isinstance(res, APICreditEntity) 83 | 84 | 85 | def test_get_package(session: Session): 86 | res = session.get_package() 87 | assert isinstance(res, PackageEntity) 88 | -------------------------------------------------------------------------------- /tests/test_websocket.py: -------------------------------------------------------------------------------- 1 | from fish_audio_sdk import TTSRequest, WebSocketSession, AsyncWebSocketSession 2 | 3 | story = """ 4 | 修炼了六千三百七十九年又三月零六天后,天门因她终于洞开。 5 | 6 | 她凭虚站立在黄山峰顶,因天门洞开而鼓起的飓风不停拍打着她身上的黑袍,在催促她快快登仙而去;黄山间壮阔的云海也随之翻涌,为这一场天地幸事欢呼雀跃。她没有抬头看向那似隐似现、若有若无、形态万千变化的天门,只是呆立在原处自顾自地看向远方。 7 | """ 8 | 9 | 10 | def test_tts(sync_websocket: WebSocketSession): 11 | buffer = bytearray() 12 | 13 | def stream(): 14 | for line in story.split("\n"): 15 | yield line 16 | 17 | for chunk in sync_websocket.tts(TTSRequest(text=""), stream()): 18 | buffer.extend(chunk) 19 | assert len(buffer) > 0 20 | 21 | 22 | async def test_async_tts(async_websocket: AsyncWebSocketSession): 23 | buffer = bytearray() 24 | 25 | async def stream(): 26 | for line in story.split("\n"): 27 | yield line 28 | 29 | async for chunk in async_websocket.tts(TTSRequest(text=""), stream()): 30 | buffer.extend(chunk) 31 | assert len(buffer) > 0 32 | --------------------------------------------------------------------------------