langchain-flowspeech 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master, main]
6
+ pull_request:
7
+ branches: [master, main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ python-version: ["3.10", "3.11", "3.12"]
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: ${{ matrix.python-version }}
22
+ - name: Install package
23
+ run: python -m pip install -e ".[test]"
24
+ - name: Run tests
25
+ run: python -m pytest
26
+
@@ -0,0 +1,25 @@
1
+ name: Publish
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ id-token: write
13
+ contents: read
14
+
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.12"
20
+ - name: Build package
21
+ run: |
22
+ python -m pip install build
23
+ python -m build
24
+ - name: Publish to PyPI
25
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,10 @@
1
+ .venv/
2
+ .pytest_cache/
3
+ __pycache__/
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+ *.pyc
8
+ .env
9
+ .env.*
10
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FlowSpeech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.4
2
+ Name: langchain-flowspeech
3
+ Version: 0.1.0
4
+ Summary: LangChain tools for FlowSpeech text-to-speech generation.
5
+ Project-URL: Homepage, https://flowspeech.io/
6
+ Project-URL: Documentation, https://flowspeech.io/docs
7
+ Project-URL: Source, https://github.com/waeckerlinfederowicz66-sketch/langchain-flowspeech
8
+ Project-URL: Issues, https://github.com/waeckerlinfederowicz66-sketch/langchain-flowspeech/issues
9
+ Author-email: FlowSpeech <support@flowspeech.io>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: ai-voice,flowspeech,langchain,text-to-speech,tts
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: httpx>=0.27
24
+ Requires-Dist: langchain-core<2.0,>=0.3
25
+ Requires-Dist: pydantic<3,>=2
26
+ Provides-Extra: test
27
+ Requires-Dist: pytest>=8; extra == 'test'
28
+ Requires-Dist: respx>=0.21; extra == 'test'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # langchain-flowspeech
32
+
33
+ `langchain-flowspeech` provides LangChain tools for generating text-to-speech audio with [FlowSpeech](https://flowspeech.io/).
34
+
35
+ FlowSpeech is a context-aware text-to-speech service for human-like narration with emotion control, pause control, and multi-speaker voice configuration.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install langchain-flowspeech
41
+ ```
42
+
43
+ ## Authentication
44
+
45
+ Create a FlowSpeech API key in your FlowSpeech account, then export it:
46
+
47
+ ```bash
48
+ export FLOWSPEECH_API_KEY="..."
49
+ ```
50
+
51
+ Do not commit API keys, cookies, session tokens, or provider credentials.
52
+
53
+ ## Usage
54
+
55
+ ```python
56
+ from langchain_flowspeech import FlowSpeechTextToSpeechTool
57
+
58
+ tool = FlowSpeechTextToSpeechTool()
59
+
60
+ result = tool.invoke({
61
+ "text": "Welcome to FlowSpeech. This audio is generated from a LangChain tool.",
62
+ "voice_name": "Kore",
63
+ "output_path": "flowspeech-demo.wav",
64
+ })
65
+
66
+ print(result["output_path"])
67
+ print(result["mime_type"])
68
+ print(result["quota"])
69
+ ```
70
+
71
+ ## Multi-Speaker Speech
72
+
73
+ ```python
74
+ from langchain_flowspeech import FlowSpeechSpeaker, FlowSpeechTextToSpeechTool
75
+
76
+ tool = FlowSpeechTextToSpeechTool()
77
+
78
+ result = tool.invoke({
79
+ "text": "Speaker A: We can make this sound natural.\nSpeaker B: Great, let's use different voices.",
80
+ "speakers": [
81
+ FlowSpeechSpeaker(speaker="Speaker A", voice_name="Kore"),
82
+ FlowSpeechSpeaker(speaker="Speaker B", voice_name="Puck"),
83
+ ],
84
+ "output_path": "conversation.wav",
85
+ })
86
+ ```
87
+
88
+ ## API Surface
89
+
90
+ The package uses the public FlowSpeech API:
91
+
92
+ - `GET /api/ai/text-to-speech/quota`
93
+ - `POST /api/ai/text-to-speech`
94
+
95
+ Responses include base64-encoded audio and metadata such as MIME type, sample rate, channel count, bit depth, and quota information.
96
+
97
+ ## Development
98
+
99
+ ```bash
100
+ python -m venv .venv
101
+ . .venv/bin/activate
102
+ pip install -e ".[test]"
103
+ pytest
104
+ ```
105
+
106
+ ## Publishing
107
+
108
+ This project is ready for PyPI packaging. Build locally with:
109
+
110
+ ```bash
111
+ python -m pip install build
112
+ python -m build
113
+ ```
114
+
115
+ For GitHub Actions trusted publishing, configure a PyPI trusted publisher for `waeckerlinfederowicz66-sketch/langchain-flowspeech`, then add a publishing workflow with `id-token: write`.
116
+
117
+ ## Safety
118
+
119
+ This repository intentionally contains no secrets. Tests use mocked HTTP responses.
@@ -0,0 +1,89 @@
1
+ # langchain-flowspeech
2
+
3
+ `langchain-flowspeech` provides LangChain tools for generating text-to-speech audio with [FlowSpeech](https://flowspeech.io/).
4
+
5
+ FlowSpeech is a context-aware text-to-speech service for human-like narration with emotion control, pause control, and multi-speaker voice configuration.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install langchain-flowspeech
11
+ ```
12
+
13
+ ## Authentication
14
+
15
+ Create a FlowSpeech API key in your FlowSpeech account, then export it:
16
+
17
+ ```bash
18
+ export FLOWSPEECH_API_KEY="..."
19
+ ```
20
+
21
+ Do not commit API keys, cookies, session tokens, or provider credentials.
22
+
23
+ ## Usage
24
+
25
+ ```python
26
+ from langchain_flowspeech import FlowSpeechTextToSpeechTool
27
+
28
+ tool = FlowSpeechTextToSpeechTool()
29
+
30
+ result = tool.invoke({
31
+ "text": "Welcome to FlowSpeech. This audio is generated from a LangChain tool.",
32
+ "voice_name": "Kore",
33
+ "output_path": "flowspeech-demo.wav",
34
+ })
35
+
36
+ print(result["output_path"])
37
+ print(result["mime_type"])
38
+ print(result["quota"])
39
+ ```
40
+
41
+ ## Multi-Speaker Speech
42
+
43
+ ```python
44
+ from langchain_flowspeech import FlowSpeechSpeaker, FlowSpeechTextToSpeechTool
45
+
46
+ tool = FlowSpeechTextToSpeechTool()
47
+
48
+ result = tool.invoke({
49
+ "text": "Speaker A: We can make this sound natural.\nSpeaker B: Great, let's use different voices.",
50
+ "speakers": [
51
+ FlowSpeechSpeaker(speaker="Speaker A", voice_name="Kore"),
52
+ FlowSpeechSpeaker(speaker="Speaker B", voice_name="Puck"),
53
+ ],
54
+ "output_path": "conversation.wav",
55
+ })
56
+ ```
57
+
58
+ ## API Surface
59
+
60
+ The package uses the public FlowSpeech API:
61
+
62
+ - `GET /api/ai/text-to-speech/quota`
63
+ - `POST /api/ai/text-to-speech`
64
+
65
+ Responses include base64-encoded audio and metadata such as MIME type, sample rate, channel count, bit depth, and quota information.
66
+
67
+ ## Development
68
+
69
+ ```bash
70
+ python -m venv .venv
71
+ . .venv/bin/activate
72
+ pip install -e ".[test]"
73
+ pytest
74
+ ```
75
+
76
+ ## Publishing
77
+
78
+ This project is ready for PyPI packaging. Build locally with:
79
+
80
+ ```bash
81
+ python -m pip install build
82
+ python -m build
83
+ ```
84
+
85
+ For GitHub Actions trusted publishing, configure a PyPI trusted publisher for `waeckerlinfederowicz66-sketch/langchain-flowspeech`, then add a publishing workflow with `id-token: write`.
86
+
87
+ ## Safety
88
+
89
+ This repository intentionally contains no secrets. Tests use mocked HTTP responses.
@@ -0,0 +1,62 @@
1
+ # FlowSpeech
2
+
3
+ [FlowSpeech](https://flowspeech.io/) is a context-aware text-to-speech service for generating human-like narration with voice, emotion, pause, and multi-speaker controls.
4
+
5
+ This package provides a LangChain tool that calls the FlowSpeech text-to-speech API and saves the generated audio to a local WAV file.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install langchain-flowspeech
11
+ ```
12
+
13
+ ## Credentials
14
+
15
+ Create a FlowSpeech API key in your FlowSpeech account and set it as an environment variable:
16
+
17
+ ```bash
18
+ export FLOWSPEECH_API_KEY="..."
19
+ ```
20
+
21
+ ## Instantiation
22
+
23
+ ```python
24
+ from langchain_flowspeech import FlowSpeechTextToSpeechTool
25
+
26
+ tool = FlowSpeechTextToSpeechTool()
27
+ ```
28
+
29
+ ## Invocation
30
+
31
+ ```python
32
+ result = tool.invoke({
33
+ "text": "Generate natural speech from a LangChain tool.",
34
+ "voice_name": "Kore",
35
+ "output_path": "flowspeech-demo.wav",
36
+ })
37
+
38
+ print(result["output_path"])
39
+ ```
40
+
41
+ ## Multi-Speaker Invocation
42
+
43
+ ```python
44
+ from langchain_flowspeech import FlowSpeechSpeaker, FlowSpeechTextToSpeechTool
45
+
46
+ tool = FlowSpeechTextToSpeechTool()
47
+
48
+ result = tool.invoke({
49
+ "text": "Speaker A: We can make this sound natural.\nSpeaker B: Great, let's use different voices.",
50
+ "speakers": [
51
+ FlowSpeechSpeaker(speaker="Speaker A", voice_name="Kore"),
52
+ FlowSpeechSpeaker(speaker="Speaker B", voice_name="Puck"),
53
+ ],
54
+ "output_path": "conversation.wav",
55
+ })
56
+ ```
57
+
58
+ ## API Reference
59
+
60
+ - `FlowSpeechTextToSpeechTool`
61
+ - `FlowSpeechClient`
62
+ - `FlowSpeechSpeaker`
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.26"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "langchain-flowspeech"
7
+ version = "0.1.0"
8
+ description = "LangChain tools for FlowSpeech text-to-speech generation."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "FlowSpeech", email = "support@flowspeech.io" }
14
+ ]
15
+ keywords = ["flowspeech", "langchain", "text-to-speech", "tts", "ai-voice"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: Multimedia :: Sound/Audio :: Speech",
25
+ "Topic :: Software Development :: Libraries :: Python Modules",
26
+ ]
27
+ dependencies = [
28
+ "httpx>=0.27",
29
+ "langchain-core>=0.3,<2.0",
30
+ "pydantic>=2,<3",
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ test = [
35
+ "pytest>=8",
36
+ "respx>=0.21",
37
+ ]
38
+
39
+ [project.urls]
40
+ Homepage = "https://flowspeech.io/"
41
+ Documentation = "https://flowspeech.io/docs"
42
+ Source = "https://github.com/waeckerlinfederowicz66-sketch/langchain-flowspeech"
43
+ Issues = "https://github.com/waeckerlinfederowicz66-sketch/langchain-flowspeech/issues"
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["src/langchain_flowspeech"]
47
+
48
+ [tool.pytest.ini_options]
49
+ testpaths = ["tests"]
50
+ pythonpath = ["src"]
@@ -0,0 +1,17 @@
1
+ from langchain_flowspeech.client import (
2
+ FlowSpeechApiError,
3
+ FlowSpeechClient,
4
+ FlowSpeechQuota,
5
+ FlowSpeechSpeaker,
6
+ FlowSpeechTtsResult,
7
+ )
8
+ from langchain_flowspeech.tools import FlowSpeechTextToSpeechTool
9
+
10
+ __all__ = [
11
+ "FlowSpeechApiError",
12
+ "FlowSpeechClient",
13
+ "FlowSpeechQuota",
14
+ "FlowSpeechSpeaker",
15
+ "FlowSpeechTextToSpeechTool",
16
+ "FlowSpeechTtsResult",
17
+ ]
@@ -0,0 +1,160 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import httpx
9
+ from pydantic import BaseModel, ConfigDict, Field
10
+
11
+
12
+ class FlowSpeechApiError(RuntimeError):
13
+ """Raised when the FlowSpeech API returns an error response."""
14
+
15
+ def __init__(self, message: str, *, status_code: int | None = None) -> None:
16
+ super().__init__(message)
17
+ self.status_code = status_code
18
+
19
+
20
+ class FlowSpeechSpeaker(BaseModel):
21
+ """Voice assignment for a single-speaker or multi-speaker request."""
22
+
23
+ speaker: str | None = Field(default=None, description="Speaker label in the input text.")
24
+ voice_name: str = Field(default="Kore", description="FlowSpeech voice name.")
25
+
26
+ def to_api(self) -> dict[str, str]:
27
+ payload = {"voiceName": self.voice_name}
28
+ if self.speaker:
29
+ payload["speaker"] = self.speaker
30
+ return payload
31
+
32
+
33
+ class FlowSpeechQuota(BaseModel):
34
+ limit: int | None = None
35
+ used: int | None = None
36
+ remaining: int | None = None
37
+ reset_at: str | None = Field(default=None, alias="resetAt")
38
+ is_guest: bool | None = Field(default=None, alias="isGuest")
39
+
40
+ model_config = ConfigDict(populate_by_name=True)
41
+
42
+
43
+ class FlowSpeechTtsResult(BaseModel):
44
+ audio_base64: str = Field(alias="audioBase64")
45
+ mime_type: str | None = Field(default=None, alias="mimeType")
46
+ quota: FlowSpeechQuota | None = None
47
+ sample_rate: int | None = Field(default=None, alias="sampleRate")
48
+ num_channels: int | None = Field(default=None, alias="numChannels")
49
+ bits_per_sample: int | None = Field(default=None, alias="bitsPerSample")
50
+
51
+ model_config = ConfigDict(populate_by_name=True)
52
+
53
+ @property
54
+ def audio_bytes(self) -> bytes:
55
+ return base64.b64decode(self.audio_base64)
56
+
57
+ def save(self, path: str | Path) -> Path:
58
+ output_path = Path(path)
59
+ output_path.parent.mkdir(parents=True, exist_ok=True)
60
+ output_path.write_bytes(self.audio_bytes)
61
+ return output_path
62
+
63
+
64
+ class FlowSpeechClient:
65
+ """Small HTTP client for the public FlowSpeech text-to-speech API."""
66
+
67
+ def __init__(
68
+ self,
69
+ *,
70
+ api_key: str | None = None,
71
+ base_url: str = "https://flowspeech.io",
72
+ timeout: float = 80.0,
73
+ http_client: httpx.Client | None = None,
74
+ ) -> None:
75
+ self.api_key = api_key or os.getenv("FLOWSPEECH_API_KEY")
76
+ if not self.api_key:
77
+ raise FlowSpeechApiError(
78
+ "FlowSpeech API key is required. Set FLOWSPEECH_API_KEY or pass api_key."
79
+ )
80
+ self.base_url = base_url.rstrip("/")
81
+ self._client = http_client or httpx.Client(timeout=timeout)
82
+ self._owns_client = http_client is None
83
+
84
+ def close(self) -> None:
85
+ if self._owns_client:
86
+ self._client.close()
87
+
88
+ def __enter__(self) -> "FlowSpeechClient":
89
+ return self
90
+
91
+ def __exit__(self, *_: object) -> None:
92
+ self.close()
93
+
94
+ def _headers(self) -> dict[str, str]:
95
+ return {
96
+ "Authorization": f"Bearer {self.api_key}",
97
+ "Content-Type": "application/json",
98
+ "User-Agent": "langchain-flowspeech/0.1.0",
99
+ }
100
+
101
+ def check_quota(self) -> FlowSpeechQuota:
102
+ response = self._client.get(
103
+ f"{self.base_url}/api/ai/text-to-speech/quota",
104
+ headers=self._headers(),
105
+ )
106
+ payload = self._read_response(response)
107
+ quota = payload.get("data", {}).get("quota")
108
+ if not isinstance(quota, dict):
109
+ raise FlowSpeechApiError("FlowSpeech quota response did not include quota data.")
110
+ return FlowSpeechQuota.model_validate(quota)
111
+
112
+ def generate_speech(
113
+ self,
114
+ *,
115
+ text: str,
116
+ speakers: list[FlowSpeechSpeaker] | None = None,
117
+ voice_name: str = "Kore",
118
+ original_text: str | None = None,
119
+ ) -> FlowSpeechTtsResult:
120
+ if not text.strip():
121
+ raise ValueError("text must not be empty")
122
+
123
+ speaker_payload = speakers or [FlowSpeechSpeaker(voice_name=voice_name)]
124
+ response = self._client.post(
125
+ f"{self.base_url}/api/ai/text-to-speech",
126
+ headers=self._headers(),
127
+ json={
128
+ "text": text,
129
+ "originalText": original_text or text,
130
+ "speakers": [speaker.to_api() for speaker in speaker_payload],
131
+ },
132
+ )
133
+ payload = self._read_response(response)
134
+ data = payload.get("data")
135
+ if not isinstance(data, dict) or not data.get("audioBase64"):
136
+ raise FlowSpeechApiError("FlowSpeech response did not include audioBase64.")
137
+ return FlowSpeechTtsResult.model_validate(data)
138
+
139
+ @staticmethod
140
+ def _read_response(response: httpx.Response) -> dict[str, Any]:
141
+ try:
142
+ payload = response.json()
143
+ except ValueError as exc:
144
+ raise FlowSpeechApiError(
145
+ "FlowSpeech API returned a non-JSON response.",
146
+ status_code=response.status_code,
147
+ ) from exc
148
+
149
+ if response.status_code >= 400:
150
+ message = payload.get("message") if isinstance(payload, dict) else None
151
+ raise FlowSpeechApiError(
152
+ message or f"FlowSpeech API returned HTTP {response.status_code}.",
153
+ status_code=response.status_code,
154
+ )
155
+
156
+ if not isinstance(payload, dict) or payload.get("code") != 0:
157
+ message = payload.get("message") if isinstance(payload, dict) else None
158
+ raise FlowSpeechApiError(message or "FlowSpeech API returned an error.")
159
+
160
+ return payload
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any, Type
5
+
6
+ from langchain_core.tools import BaseTool
7
+ from pydantic import BaseModel, Field, PrivateAttr
8
+
9
+ from langchain_flowspeech.client import FlowSpeechClient, FlowSpeechSpeaker
10
+
11
+
12
+ class FlowSpeechTextToSpeechInput(BaseModel):
13
+ text: str = Field(description="Text or speaker-labelled dialogue to synthesize.")
14
+ voice_name: str = Field(default="Kore", description="Default FlowSpeech voice name.")
15
+ speakers: list[FlowSpeechSpeaker] | None = Field(
16
+ default=None,
17
+ description="Optional explicit speaker-to-voice assignments for dialogue.",
18
+ )
19
+ output_path: str = Field(
20
+ default="flowspeech-output.wav",
21
+ description="Local path where the generated WAV audio should be saved.",
22
+ )
23
+
24
+
25
+ class FlowSpeechTextToSpeechTool(BaseTool):
26
+ """LangChain tool that generates speech audio with FlowSpeech."""
27
+
28
+ name: str = "flowspeech_text_to_speech"
29
+ description: str = (
30
+ "Generate human-like text-to-speech audio with FlowSpeech and save it as a WAV file."
31
+ )
32
+ args_schema: Type[BaseModel] = FlowSpeechTextToSpeechInput
33
+
34
+ api_key: str | None = Field(default=None, exclude=True)
35
+ base_url: str = "https://flowspeech.io"
36
+ timeout: float = 80.0
37
+
38
+ _client: FlowSpeechClient | None = PrivateAttr(default=None)
39
+
40
+ def _get_client(self) -> FlowSpeechClient:
41
+ if self._client is None:
42
+ self._client = FlowSpeechClient(
43
+ api_key=self.api_key,
44
+ base_url=self.base_url,
45
+ timeout=self.timeout,
46
+ )
47
+ return self._client
48
+
49
+ def _run(
50
+ self,
51
+ text: str,
52
+ voice_name: str = "Kore",
53
+ speakers: list[FlowSpeechSpeaker] | None = None,
54
+ output_path: str = "flowspeech-output.wav",
55
+ **_: Any,
56
+ ) -> dict[str, Any]:
57
+ result = self._get_client().generate_speech(
58
+ text=text,
59
+ voice_name=voice_name,
60
+ speakers=speakers,
61
+ )
62
+ saved_path = result.save(output_path)
63
+ return {
64
+ "output_path": str(Path(saved_path)),
65
+ "mime_type": result.mime_type,
66
+ "sample_rate": result.sample_rate,
67
+ "num_channels": result.num_channels,
68
+ "bits_per_sample": result.bits_per_sample,
69
+ "quota": result.quota.model_dump(by_alias=True) if result.quota else None,
70
+ }
71
+
72
+ async def _arun(self, *_: Any, **__: Any) -> dict[str, Any]:
73
+ raise NotImplementedError("FlowSpeechTextToSpeechTool currently supports sync use.")
@@ -0,0 +1,89 @@
1
+ import base64
2
+
3
+ import httpx
4
+ import pytest
5
+
6
+ from langchain_flowspeech import FlowSpeechApiError, FlowSpeechClient, FlowSpeechSpeaker
7
+
8
+
9
+ def test_generate_speech_sends_public_api_shape() -> None:
10
+ captured = {}
11
+
12
+ def handler(request: httpx.Request) -> httpx.Response:
13
+ captured["url"] = str(request.url)
14
+ captured["auth"] = request.headers.get("authorization")
15
+ captured["json"] = request.read().decode()
16
+ return httpx.Response(
17
+ 200,
18
+ json={
19
+ "code": 0,
20
+ "message": "ok",
21
+ "data": {
22
+ "audioBase64": base64.b64encode(b"RIFFdemo").decode(),
23
+ "mimeType": "audio/L16;rate=24000",
24
+ "sampleRate": 24000,
25
+ "numChannels": 1,
26
+ "bitsPerSample": 16,
27
+ "quota": {
28
+ "limit": 10000,
29
+ "used": 10,
30
+ "remaining": 9990,
31
+ "resetAt": "2026-07-31T15:59:59.999Z",
32
+ "isGuest": False,
33
+ },
34
+ },
35
+ },
36
+ )
37
+
38
+ transport = httpx.MockTransport(handler)
39
+ with httpx.Client(transport=transport) as http_client:
40
+ client = FlowSpeechClient(
41
+ api_key="test-key",
42
+ base_url="https://flowspeech.io",
43
+ http_client=http_client,
44
+ )
45
+ result = client.generate_speech(
46
+ text="Speaker A: Hello",
47
+ speakers=[FlowSpeechSpeaker(speaker="Speaker A", voice_name="Kore")],
48
+ )
49
+
50
+ assert captured["url"] == "https://flowspeech.io/api/ai/text-to-speech"
51
+ assert captured["auth"] == "Bearer test-key"
52
+ assert '"originalText":"Speaker A: Hello"' in captured["json"]
53
+ assert '"speaker":"Speaker A"' in captured["json"]
54
+ assert result.audio_bytes == b"RIFFdemo"
55
+ assert result.quota is not None
56
+ assert result.quota.remaining == 9990
57
+
58
+
59
+ def test_check_quota() -> None:
60
+ def handler(_: httpx.Request) -> httpx.Response:
61
+ return httpx.Response(
62
+ 200,
63
+ json={
64
+ "code": 0,
65
+ "data": {
66
+ "quota": {
67
+ "limit": 100,
68
+ "used": 25,
69
+ "remaining": 75,
70
+ "resetAt": "2026-07-31T15:59:59.999Z",
71
+ "isGuest": False,
72
+ }
73
+ },
74
+ },
75
+ )
76
+
77
+ with httpx.Client(transport=httpx.MockTransport(handler)) as http_client:
78
+ client = FlowSpeechClient(api_key="test-key", http_client=http_client)
79
+ quota = client.check_quota()
80
+
81
+ assert quota.limit == 100
82
+ assert quota.remaining == 75
83
+
84
+
85
+ def test_requires_api_key(monkeypatch: pytest.MonkeyPatch) -> None:
86
+ monkeypatch.delenv("FLOWSPEECH_API_KEY", raising=False)
87
+
88
+ with pytest.raises(FlowSpeechApiError, match="API key is required"):
89
+ FlowSpeechClient()
@@ -0,0 +1,36 @@
1
+ import base64
2
+
3
+ import httpx
4
+
5
+ from langchain_flowspeech.client import FlowSpeechClient
6
+ from langchain_flowspeech.tools import FlowSpeechTextToSpeechTool
7
+
8
+
9
+ def test_tool_saves_audio(tmp_path) -> None:
10
+ def handler(_: httpx.Request) -> httpx.Response:
11
+ return httpx.Response(
12
+ 200,
13
+ json={
14
+ "code": 0,
15
+ "data": {
16
+ "audioBase64": base64.b64encode(b"RIFFtool").decode(),
17
+ "mimeType": "audio/L16;rate=24000",
18
+ "sampleRate": 24000,
19
+ "numChannels": 1,
20
+ "bitsPerSample": 16,
21
+ },
22
+ },
23
+ )
24
+
25
+ output_path = tmp_path / "speech.wav"
26
+ tool = FlowSpeechTextToSpeechTool(api_key="test-key")
27
+ tool._client = FlowSpeechClient(
28
+ api_key="test-key",
29
+ http_client=httpx.Client(transport=httpx.MockTransport(handler)),
30
+ )
31
+
32
+ result = tool.invoke({"text": "Hello from FlowSpeech", "output_path": str(output_path)})
33
+
34
+ assert output_path.read_bytes() == b"RIFFtool"
35
+ assert result["output_path"] == str(output_path)
36
+ assert result["sample_rate"] == 24000