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.
- langchain_flowspeech-0.1.0/.github/workflows/ci.yml +26 -0
- langchain_flowspeech-0.1.0/.github/workflows/publish.yml +25 -0
- langchain_flowspeech-0.1.0/.gitignore +10 -0
- langchain_flowspeech-0.1.0/LICENSE +21 -0
- langchain_flowspeech-0.1.0/PKG-INFO +119 -0
- langchain_flowspeech-0.1.0/README.md +89 -0
- langchain_flowspeech-0.1.0/docs/langchain/flowspeech.mdx +62 -0
- langchain_flowspeech-0.1.0/pyproject.toml +50 -0
- langchain_flowspeech-0.1.0/src/langchain_flowspeech/__init__.py +17 -0
- langchain_flowspeech-0.1.0/src/langchain_flowspeech/client.py +160 -0
- langchain_flowspeech-0.1.0/src/langchain_flowspeech/tools.py +73 -0
- langchain_flowspeech-0.1.0/tests/test_client.py +89 -0
- langchain_flowspeech-0.1.0/tests/test_tools.py +36 -0
|
@@ -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,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
|