tts-pace 0.3.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.
- tts_pace-0.3.0/.env.example +7 -0
- tts_pace-0.3.0/.github/ISSUE_TEMPLATE/bug.yml +22 -0
- tts_pace-0.3.0/.github/ISSUE_TEMPLATE/question.yml +11 -0
- tts_pace-0.3.0/.github/workflows/ci.yml +20 -0
- tts_pace-0.3.0/.gitignore +43 -0
- tts_pace-0.3.0/.python-version +1 -0
- tts_pace-0.3.0/CHANGELOG.md +54 -0
- tts_pace-0.3.0/LICENSE +21 -0
- tts_pace-0.3.0/PKG-INFO +401 -0
- tts_pace-0.3.0/README.ko.md +373 -0
- tts_pace-0.3.0/README.md +381 -0
- tts_pace-0.3.0/SECURITY.md +7 -0
- tts_pace-0.3.0/docs/images/01-demo.png +0 -0
- tts_pace-0.3.0/docs/images/02-count.png +0 -0
- tts_pace-0.3.0/docs/images/03-measure.png +0 -0
- tts_pace-0.3.0/docs/images/04-fit.png +0 -0
- tts_pace-0.3.0/docs/images/05-quickstart.png +0 -0
- tts_pace-0.3.0/examples/quickstart.py +16 -0
- tts_pace-0.3.0/pyproject.toml +40 -0
- tts_pace-0.3.0/scripts/make_cmudict_sample.py +47 -0
- tts_pace-0.3.0/src/tts_pace/__init__.py +32 -0
- tts_pace-0.3.0/src/tts_pace/audio.py +96 -0
- tts_pace-0.3.0/src/tts_pace/cache.py +85 -0
- tts_pace-0.3.0/src/tts_pace/cli.py +194 -0
- tts_pace-0.3.0/src/tts_pace/count.py +99 -0
- tts_pace-0.3.0/src/tts_pace/demo.py +60 -0
- tts_pace-0.3.0/src/tts_pace/en_syllables.py +99 -0
- tts_pace-0.3.0/src/tts_pace/engines/__init__.py +9 -0
- tts_pace-0.3.0/src/tts_pace/engines/base.py +36 -0
- tts_pace-0.3.0/src/tts_pace/engines/callable.py +56 -0
- tts_pace-0.3.0/src/tts_pace/engines/mock.py +113 -0
- tts_pace-0.3.0/src/tts_pace/engines/piper.py +69 -0
- tts_pace-0.3.0/src/tts_pace/engines/say.py +76 -0
- tts_pace-0.3.0/src/tts_pace/errors.py +27 -0
- tts_pace-0.3.0/src/tts_pace/fit.py +406 -0
- tts_pace-0.3.0/src/tts_pace/measure.py +149 -0
- tts_pace-0.3.0/src/tts_pace/normalize.py +172 -0
- tts_pace-0.3.0/src/tts_pace/py.typed +0 -0
- tts_pace-0.3.0/src/tts_pace/runtime.py +112 -0
- tts_pace-0.3.0/src/tts_pace/tempo.py +130 -0
- tts_pace-0.3.0/tests/conftest.py +30 -0
- tts_pace-0.3.0/tests/fixtures/CMUDICT-LICENSE +33 -0
- tts_pace-0.3.0/tests/fixtures/cmudict_sample.tsv +2000 -0
- tts_pace-0.3.0/tests/test_cache.py +75 -0
- tts_pace-0.3.0/tests/test_cli.py +192 -0
- tts_pace-0.3.0/tests/test_count.py +67 -0
- tts_pace-0.3.0/tests/test_count_en_accuracy.py +30 -0
- tts_pace-0.3.0/tests/test_engines.py +106 -0
- tts_pace-0.3.0/tests/test_fit.py +86 -0
- tts_pace-0.3.0/tests/test_measure.py +58 -0
- tts_pace-0.3.0/tests/test_real.py +30 -0
- tts_pace-0.3.0/tests/test_runtime.py +93 -0
- tts_pace-0.3.0/tests/test_target_duration.py +183 -0
- tts_pace-0.3.0/tests/test_tempo.py +52 -0
- tts_pace-0.3.0/uv.lock +156 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# No API key is needed. All variables are optional.
|
|
2
|
+
# Where fit() stores cached results when caching is enabled (CLI default: $XDG_CACHE_HOME/tts-pace or ~/.cache/tts-pace; the library fit() caches only when cache_dir is given or this variable is set)
|
|
3
|
+
TTS_PACE_CACHE_DIR=
|
|
4
|
+
# Path to the ffmpeg executable (default: search PATH; without it the numpy backends are used)
|
|
5
|
+
TTS_PACE_FFMPEG=
|
|
6
|
+
# Path to a Piper voice .onnx file (only for `--engine piper`)
|
|
7
|
+
TTS_PACE_PIPER_VOICE=
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: 버그 / Bug
|
|
2
|
+
description: README대로 했는데 다르게 동작해요 / Something does not work as the README says
|
|
3
|
+
labels: [bug]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: what
|
|
7
|
+
attributes:
|
|
8
|
+
label: 무엇이 일어났나요? / What happened?
|
|
9
|
+
description: 실행한 명령과 나온 메시지를 그대로 붙여 주세요. / Paste the command and the output.
|
|
10
|
+
validations:
|
|
11
|
+
required: true
|
|
12
|
+
- type: textarea
|
|
13
|
+
id: expected
|
|
14
|
+
attributes:
|
|
15
|
+
label: 기대한 결과 / Expected
|
|
16
|
+
- type: input
|
|
17
|
+
id: env
|
|
18
|
+
attributes:
|
|
19
|
+
label: 환경 / Environment
|
|
20
|
+
placeholder: "macOS 15 (M3) · Ubuntu 24.04 · Windows 11 / uv 0.x / mock or real mode"
|
|
21
|
+
validations:
|
|
22
|
+
required: true
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
name: 질문 / Question
|
|
2
|
+
description: 이해가 안 되는 부분이나 더 알고 싶은 점 / Something unclear or worth explaining
|
|
3
|
+
labels: [question]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: question
|
|
7
|
+
attributes:
|
|
8
|
+
label: 질문 / Question
|
|
9
|
+
description: README의 어느 부분(절 제목)에 대한 질문인지 알려 주세요. / Which README section is this about?
|
|
10
|
+
validations:
|
|
11
|
+
required: true
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# 키·모델 없이 mock 모드로 전체 흐름을 검증한다.
|
|
2
|
+
name: ci
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
schedule:
|
|
7
|
+
- cron: "17 3 * * 1" # 매주 월요일 — 의존성·런타임이 바뀌어 깨지지 않았는지 정기 확인
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: astral-sh/setup-uv@v6
|
|
15
|
+
- run: uv python install
|
|
16
|
+
- run: uv sync --locked
|
|
17
|
+
- run: uv run pytest -q
|
|
18
|
+
env:
|
|
19
|
+
DEMO_MODE: mock
|
|
20
|
+
- run: uv run tts-pace demo
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
.venv/
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
.ruff_cache/
|
|
7
|
+
.mypy_cache/
|
|
8
|
+
*.egg-info/
|
|
9
|
+
|
|
10
|
+
# Node
|
|
11
|
+
/node_modules/
|
|
12
|
+
/dist/
|
|
13
|
+
/.next/
|
|
14
|
+
|
|
15
|
+
# 실행 산출물·모델·데이터 캐시
|
|
16
|
+
/runs/
|
|
17
|
+
/outputs/
|
|
18
|
+
/.cache/
|
|
19
|
+
*.safetensors
|
|
20
|
+
*.gguf
|
|
21
|
+
*.bin
|
|
22
|
+
|
|
23
|
+
# 환경·비밀
|
|
24
|
+
.env
|
|
25
|
+
.env.*
|
|
26
|
+
!.env.example
|
|
27
|
+
|
|
28
|
+
# OS·에디터
|
|
29
|
+
.DS_Store
|
|
30
|
+
.vscode/
|
|
31
|
+
.idea/
|
|
32
|
+
|
|
33
|
+
# Flutter·Dart
|
|
34
|
+
.dart_tool/
|
|
35
|
+
/build/
|
|
36
|
+
.flutter-plugins
|
|
37
|
+
.flutter-plugins-dependencies
|
|
38
|
+
/coverage/
|
|
39
|
+
|
|
40
|
+
# tts-pace
|
|
41
|
+
/tts-pace-demo/
|
|
42
|
+
/quickstart.wav
|
|
43
|
+
/*.wav
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [0.3.0] - 2026-11-11
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `tts-pace fit --json` prints the report JSON (same structure as `retime --json`) to stdout as a single line.
|
|
11
|
+
The practice-mode `[mock]` notice and warnings go to stderr; errors still exit 2 with an empty stdout.
|
|
12
|
+
|
|
13
|
+
### Known issues
|
|
14
|
+
- Tests for `--json` do not yet cover `--lang` or `--target-duration` with `--cache-dir`.
|
|
15
|
+
- On a `--json` error in mock mode, the first stderr line is the `[mock]` notice, followed by the `error:` line (stdout stays empty, exit 2).
|
|
16
|
+
|
|
17
|
+
### Unchanged
|
|
18
|
+
- Without `--json`, output, reports and exit codes are identical to 0.2.0.
|
|
19
|
+
|
|
20
|
+
## [0.2.0] - 2026-11-11
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- `target_duration` (seconds, total length including pauses) for `fit()` and `fit_audio()`, and
|
|
24
|
+
`--target-duration SECONDS` for `tts-pace fit` and `retime` (mutually exclusive with `--target`).
|
|
25
|
+
Reports gain `target_duration`, `result_total_s` and `length_err_pct` in this mode only.
|
|
26
|
+
- `target_sps` may be `None` when `target_duration` is given.
|
|
27
|
+
|
|
28
|
+
### Known issues
|
|
29
|
+
- With `--target-duration 0` (or negative) the `error:` line is followed by the mock-engine banner; exit code is still 2.
|
|
30
|
+
- Some length-mode tests assert loosely and are to be tightened.
|
|
31
|
+
|
|
32
|
+
### Unchanged
|
|
33
|
+
- Without the new option, output, reports and cache keys are identical to 0.1.1.
|
|
34
|
+
|
|
35
|
+
## [0.1.1] - 2026-11-10
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- README: replaced the feature comparison table with a short "when to use" section (documentation only).
|
|
39
|
+
|
|
40
|
+
## [0.1.0] - 2026-11-09
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
- Syllable counting for Korean (blocks), English (vowel-group heuristic plus a small exception
|
|
44
|
+
dictionary) and Japanese (morae), with spoken-form normalization of numbers and acronyms.
|
|
45
|
+
- Articulation-rate measurement: syllables divided by speech time without long silences
|
|
46
|
+
(numpy backend, optional ffmpeg `silencedetect` backend).
|
|
47
|
+
- Pitch-preserving tempo change limited to 0.85-1.15 (numpy WSOLA, optional ffmpeg `atempo`).
|
|
48
|
+
- Closed-loop `fit()` and engine-free `fit_audio()` with warnings `knob_response_weak`,
|
|
49
|
+
`knob_clamped`, `tempo_clamped`, `unsupported_chars`.
|
|
50
|
+
- Deterministic result cache with an entry limit.
|
|
51
|
+
- Engine adapters: `MockEngine` (practice engine, not a real TTS), `CallableEngine`, `SayEngine`,
|
|
52
|
+
`PiperEngine`.
|
|
53
|
+
- Concurrency `Limiter`, per-run temp folders, subprocess timeouts.
|
|
54
|
+
- `tts-pace` CLI: `demo`, `count`, `measure`, `retime`, `fit`.
|
tts_pace-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LeeGyeongPil
|
|
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.
|
tts_pace-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tts-pace
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Measure speaking rate (syllables per second of pure speech) and fit TTS output to a target pace: engine knob re-synthesis plus a bounded, pitch-preserving tempo change.
|
|
5
|
+
Project-URL: Homepage, https://github.com/LeeGyeongPil/tts-pace
|
|
6
|
+
Project-URL: Issues, https://github.com/LeeGyeongPil/tts-pace/issues
|
|
7
|
+
Author: LeeGyeongPil
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: audio,dubbing,speaking-rate,speech,syllables,tts
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Requires-Dist: numpy>=2.0
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# tts-pace
|
|
22
|
+
|
|
23
|
+
**English** | [한국어](https://github.com/LeeGyeongPil/tts-pace/blob/v0.3.0/README.ko.md)
|
|
24
|
+
|
|
25
|
+
A small Python library and CLI that measures the speaking rate of speech audio (syllables per second of *pure speech*, silences excluded) and fits a TTS engine's output to a target pace: first with the engine's own speed knob, then with a bounded, pitch-preserving tempo change. Use it when a line must finish within a set time, as in video dubbing, narration or reading aloud; you can also give a target total length in seconds (`--target-duration`) instead of a pace. One runtime dependency (numpy), no API key, and a practice (mock) engine so you can try everything offline. Install: `uv add git+https://github.com/LeeGyeongPil/tts-pace`.
|
|
26
|
+
|
|
27
|
+
By analogy, a TTS speed setting is a faucet with no markings: you cannot tell whether turning it further gives proportionally more water. tts-pace fits a meter to the faucet and turns it a little again by hand while reading the dial.
|
|
28
|
+
|
|
29
|
+

|
|
30
|
+
|
|
31
|
+
*Result of running `tts-pace demo` (practice mock engine). Three of the four cases are within 0.5% error, and the last line is a case that hit a limit and raised warnings.*
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# To use as a library (add to your project)
|
|
37
|
+
uv add git+https://github.com/LeeGyeongPil/tts-pace
|
|
38
|
+
|
|
39
|
+
# To clone the repository and try it right away
|
|
40
|
+
git clone https://github.com/LeeGyeongPil/tts-pace && cd tts-pace && uv sync
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- Requirements: Python 3.12 or later, [uv](https://docs.astral.sh/uv/) (if missing, `curl -LsSf https://astral.sh/uv/install.sh | sh`). On a minimal Linux container you only need `git` and `curl` in addition.
|
|
44
|
+
- The runtime dependency is **numpy alone**. ffmpeg, `say` and Piper are optional.
|
|
45
|
+
- No API key is needed. All environment variables are optional and are listed in [.env.example](https://github.com/LeeGyeongPil/tts-pace/blob/v0.3.0/.env.example).
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
This is `examples/quickstart.py`. It runs as is when copied.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from tts_pace import MockEngine, fit
|
|
53
|
+
|
|
54
|
+
result = fit(
|
|
55
|
+
"Today we learn how to match the speaking rate of a synthetic voice.",
|
|
56
|
+
target_sps=4.0,
|
|
57
|
+
engine=MockEngine("en"),
|
|
58
|
+
out="quickstart.wav",
|
|
59
|
+
)
|
|
60
|
+
print(f"path={result.best.path} err={result.best.err_pct:.2f}% (mock engine, route={result.route})")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
uv run python examples/quickstart.py
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
path=quickstart.wav err=0.11% (mock engine, route=resynth+tempo)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+

|
|
72
|
+
|
|
73
|
+
*`MockEngine` is a **practice mock engine**. It is not real speech; it merely produces short sound chunks and pauses, one chunk per syllable, but the measurement and tempo-change code goes through exactly the same path as with a real one. Real TTS is swapped in with `SayEngine`, `PiperEngine` or `CallableEngine` (see [API](#api) below).*
|
|
74
|
+
|
|
75
|
+
## Core concepts
|
|
76
|
+
|
|
77
|
+
1. **Speaking rate (sps)**: the number of syllables divided by the *speech time with pauses removed* (syllables per second). Dividing by the character count or by the total length makes the value wobble because of pauses between sentences, so silences lasting 0.6 seconds or more are subtracted from the time.
|
|
78
|
+
2. **Speed knob**: the "faster/slower" setting a TTS engine offers. But even if you give 1.2x, there is no guarantee the actual speed becomes 1.2x (it is non-linear and stops at a floor). So it **measures to confirm**.
|
|
79
|
+
3. **Pitch-preserving tempo change**: post-processing that changes only the playback speed without changing the voice's pitch. Large changes damage the sound, so it is limited to 0.85-1.15x and used only for the final fine adjustment.
|
|
80
|
+
4. **Closed-loop control**: "synthesize → measure → compare → correct" runs as a closed loop, in the same way an air conditioner measures the temperature and adjusts its power.
|
|
81
|
+
|
|
82
|
+
```mermaid
|
|
83
|
+
flowchart LR
|
|
84
|
+
T["Text"] --> C["Count syllables"]
|
|
85
|
+
T --> S["Synthesize with engine"]
|
|
86
|
+
S --> M["Measure: syllables / speech time"]
|
|
87
|
+
C --> M
|
|
88
|
+
M --> D{"Close to the target?"}
|
|
89
|
+
D -- Yes --> P["Tempo change only"]
|
|
90
|
+
D -- No --> R["Resynthesize once with the speed knob"]
|
|
91
|
+
R --> P
|
|
92
|
+
P --> O["Result WAV + report"]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Walking through the diagram step by step:
|
|
96
|
+
|
|
97
|
+
- **Counting syllables**: after converting numbers and abbreviations to how they are read aloud (`2026` → `이천이십육`, "two thousand twenty-six"), it counts Hangul blocks, English vowel groups and Japanese morae.
|
|
98
|
+
- **Measuring**: dividing by the speech time with pauses removed gives sps.
|
|
99
|
+
- **Deciding**: if the error of the base synthesis is within 10%, it leaves the knob alone and only changes the tempo (`tempo-only`). Otherwise it adjusts the knob once and resynthesizes (`resynth`), then closes the remaining error with the tempo (`resynth+tempo`).
|
|
100
|
+
- **Result**: it picks the candidate closest to the target, and if a limit was hit it returns a warning and advice text along with it.
|
|
101
|
+
|
|
102
|
+
## Step-by-step walkthrough
|
|
103
|
+
|
|
104
|
+
Run these in order from the folder where you cloned the repository (all without keys, with the mock engine, in about 0.2 seconds each).
|
|
105
|
+
|
|
106
|
+
### 1. Run the demo in one go
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
uv run tts-pace demo
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```text
|
|
113
|
+
[mock] 연습용 목(mock) 엔진 — 실제 TTS 아님 (practice engine, not a real TTS)
|
|
114
|
+
case base sps route final sps target err warnings
|
|
115
|
+
---------------------------------------------------------------------
|
|
116
|
+
en far 4.61 resynth+tempo 4.00 4.0 0.09% -
|
|
117
|
+
en near 4.61 tempo-only 4.89 4.9 0.31% -
|
|
118
|
+
ko near 5.40 tempo-only 5.69 5.7 0.23% -
|
|
119
|
+
ko floor 5.40 resynth+tempo 4.16 4.0 3.95% knob_response_weak, tempo_clamped
|
|
120
|
+
|
|
121
|
+
wrote tts-pace-demo/ (WAV files + report.json: tts-pace-demo/report.json)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
(The first line's Korean part means "practice mock engine, not a real TTS".)
|
|
125
|
+
|
|
126
|
+
What it means: `base sps` is the speed of the first synthesis, `route` is the method used to fit, and `err` is the difference from the target. `en far` had a distant target so it used the knob as well, while `en near` and `ko near` were close and used the tempo only. `ko floor` is a case where this fake Korean engine is built so that it cannot get slower than its speed floor, so 3.95% error remains and warnings are attached. The generated WAV files and `report.json` are saved in `tts-pace-demo/`.
|
|
127
|
+
|
|
128
|
+
### 2. Counting syllables
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
uv run tts-pace count "2026년 TTS 속도를 맞춥니다"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
spoken form : 이천이십육년 티티에스 속도를 맞춥니다
|
|
136
|
+
syllables : 17 {'ko': 17, 'en': 0, 'ja': 0}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
(The input means "We adjust the TTS speed in 2026".)
|
|
140
|
+
|
|
141
|
+

|
|
142
|
+
|
|
143
|
+
*After numbers and abbreviations are converted to how they are read, 17 syllables are counted.*
|
|
144
|
+
|
|
145
|
+
What it means: TTS reads `2026` as "이천이십육", so counting the characters literally would skew the speed. It counts based on the form converted to how it is read (`spoken form`).
|
|
146
|
+
|
|
147
|
+
### 3. Measuring the speaking rate of a recording
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
uv run tts-pace measure tts-pace-demo/en_far_base.wav --text-file tts-pace-demo/en.txt
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```text
|
|
154
|
+
total 8.00s silence 1.71s (2 gaps) speech 6.30s
|
|
155
|
+
29 syllables -> 4.61 syllables/second of pure speech
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+

|
|
159
|
+
|
|
160
|
+
*Of the total 8.00 seconds, 1.71 seconds of pauses are removed, and 29 syllables were spoken in 6.30 seconds, so it is 4.61 sps.*
|
|
161
|
+
|
|
162
|
+
What it means: dividing by the total length (8.00 seconds) would give a slow-looking 3.6 sps or so, but the pauses between sentences are not speaking rate. The value with pauses removed is the basis for comparison.
|
|
163
|
+
|
|
164
|
+
### 4. Fitting to a target (a case that raises warnings)
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
uv run tts-pace fit --engine mock --lang ko \
|
|
168
|
+
--text "오늘은 말하기 속도를 맞추는 방법을 알아봅니다." \
|
|
169
|
+
--target 4.0 -o ko.wav --no-cache
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
```text
|
|
173
|
+
warning: knob_response_weak - The engine speed knob changed the pace much less than asked (it may stop at a floor or scale non-linearly). Try another voice or engine setting, or edit the text.
|
|
174
|
+
warning: tempo_clamped - The tempo change hit its limit, so the target was not reached. Shorten or lengthen the text, or choose an engine setting closer to the target pace.
|
|
175
|
+
[mock] 연습용 목(mock) 엔진 — 실제 TTS 아님 (practice engine, not a real TTS)
|
|
176
|
+
base 5.40 sps
|
|
177
|
+
resynth speed x0.740 tempo x1.000 -> 4.89 sps err 22.23%
|
|
178
|
+
resynth+tempo speed x0.740 tempo x0.850 -> 4.16 sps err 4.08%
|
|
179
|
+
best resynth+tempo: 4.16 sps (target 4, err 4.08%)
|
|
180
|
+
cache miss
|
|
181
|
+
wrote ko.wav
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
(The input text means "Today we learn how to match the speaking pace.")
|
|
185
|
+
|
|
186
|
+

|
|
187
|
+
|
|
188
|
+
*When aiming for 4.0 sps from 5.40 sps: even lowering the speed knob to 0.74x only brings it down to 4.89 sps, and even using the tempo up to its limit (0.85x) it stops at 4.16 sps.*
|
|
189
|
+
|
|
190
|
+
What it means: the knob was set to 0.74x but the speed did not drop by that much (`knob_response_weak`). The tempo change also hit the 0.85x floor (`tempo_clamped`). In such a case the tool does not stretch things by force; it returns the result closest to the target along with warnings. As advised, you need to adjust the sentence length or the target.
|
|
191
|
+
|
|
192
|
+
Instead of a speed, you can also give the target as "how many seconds in total, pauses included". `--target` and `--target-duration` cannot be used together, and one of them must be given (otherwise exit code 2).
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
uv run tts-pace fit --engine mock --text "Today we learn how to match the rate." \
|
|
196
|
+
--target-duration 4 -o o.wav --no-cache
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
```text
|
|
200
|
+
warning: tempo_clamped - The tempo change hit its limit, so the target was not reached. Shorten or lengthen the text, or choose an engine setting closer to the target pace.
|
|
201
|
+
[mock] 연습용 목(mock) 엔진 — 실제 TTS 아님 (practice engine, not a real TTS)
|
|
202
|
+
base 4.60 sps
|
|
203
|
+
resynth speed x0.714 tempo x1.000 -> 3.77 sps err 14.60%
|
|
204
|
+
resynth+tempo speed x0.714 tempo x0.850 -> 3.22 sps err 2.15%
|
|
205
|
+
best resynth+tempo: 3.22 sps (target 3.2864, err 2.15%)
|
|
206
|
+
length 3.85s (target 4.00s, err 3.85%)
|
|
207
|
+
cache miss
|
|
208
|
+
wrote o.wav
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
What it means: it stopped at 3.85 seconds for a 4.00-second target. The tempo hit the 0.85x floor (`tempo_clamped`), and the `length` line tells you the remaining difference. The `length` line appears only when `--target-duration` is used.
|
|
212
|
+
|
|
213
|
+
The `target 3.2864` on the `best` line is the target sps obtained by converting the 4-second target length into this sentence's speaking rate with pauses excluded (that is why it has a different number of decimal places from the other lines).
|
|
214
|
+
|
|
215
|
+
For use in scripts, add `--json` (since 0.3.0). Only one line of report JSON goes to standard output, and warnings and the `[mock]` notice go to standard error. It is the same content as what is saved in the `--report` file.
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
uv run tts-pace fit --engine mock --text "Today we learn pace." \
|
|
219
|
+
--target 4 -o o.wav --no-cache --json 2>/dev/null
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
```text
|
|
223
|
+
{"version": 1, "engine": "mock-en", "mock": true, "mock_note": "practice (mock) engine, not a real TTS", "target_sps": 4.0, ...
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
(Only the beginning of the output is shown. It continues with `syllables`, `route`, `best`, `candidates`, `warnings`, `cache_hit` and so on.)
|
|
227
|
+
|
|
228
|
+
The cache defaults differ between the CLI and the library. The CLI caches in `~/.cache/tts-pace` by default, and the library's `fit()` does not cache unless you give `cache_dir` (or the environment variable `TTS_PACE_CACHE_DIR`).
|
|
229
|
+
|
|
230
|
+
If you use `--cache-dir "$HOME/.cache/tts-pace"` instead of `--no-cache` in the same command (the default cache location is `~/.cache/tts-pace`, changeable with `TTS_PACE_CACHE_DIR`), `cache hit` appears from the second run and synthesis is skipped.
|
|
231
|
+
|
|
232
|
+
### 5. Fitting an existing recording with the tempo alone
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
uv run tts-pace retime tts-pace-demo/en_far_base.wav --text-file tts-pace-demo/en.txt --target 4.2 -o slower.wav
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
To fit by total length, use `--target-duration 9` instead of `--target 4.2`. This file is originally about 8 seconds, so this stretches it in the slower (longer) direction.
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
uv run tts-pace retime tts-pace-demo/en_far_base.wav --text-file tts-pace-demo/en.txt --target-duration 9 -o slower.wav
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
```text
|
|
245
|
+
base 4.61 sps
|
|
246
|
+
tempo-only speed x1.000 tempo x0.889 -> 4.10 sps err 0.01%
|
|
247
|
+
best tempo-only: 4.10 sps (target 4.09641, err 0.01%)
|
|
248
|
+
length 8.97s (target 9.00s, err 0.30%)
|
|
249
|
+
cache miss
|
|
250
|
+
wrote slower.wav
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
This is the command to use when you have only a file and no engine. If the target is too far (for example `--target 2.0`), it stops at the 0.85x floor and raises a `tempo_clamped` warning. The exit code is still 0 in that case, so in automation check for warnings in the `warnings` field of the `--json` output (both `retime --json` and `fit --json`) or in the report.
|
|
254
|
+
|
|
255
|
+
## API
|
|
256
|
+
|
|
257
|
+
These are the public names exported by `tts_pace`. Do not use module-internal functions that are not listed here.
|
|
258
|
+
|
|
259
|
+
| Name | What it does | Main arguments | Returns |
|
|
260
|
+
|---|---|---|---|
|
|
261
|
+
| `count_syllables(text, lang="auto", normalize=None)` | Syllable count based on how the text is read | `lang`: `auto/ko/en/ja` (affects only the reading of numbers and abbreviations), `normalize`: `{"2026": "twenty twenty six"}` | `SyllableCount` (`total`, `by_script`, `normalized_text`, `unsupported`) |
|
|
262
|
+
| `measure(audio, text=None, syllables=None, *, silence_db=-60.0, min_silence=0.6, backend="auto", limiter=None)` | Measures speech time with pauses removed, and speaking rate | `audio`: a WAV path or an `AudioClip`; `text` and `syllables` cannot be given together | `Measurement` (`total_s`, `silence_s`, `net_s`, `gaps`, `sps`) |
|
|
263
|
+
| `retime(audio, factor, out=None, backend="auto", clamp=False, *, tempo_range=(0.85, 1.15), limiter=None)` | Changes speed while keeping pitch (1.1 = 10% faster) | A `factor` outside the range raises `ValueError`; with `clamp=True` it is cut to the boundary | `AudioClip` |
|
|
264
|
+
| `fit(text, target_sps, engine, out=None, *, target_duration=None, tolerance=0.10, tempo_range, cache_dir=None, lang="auto", normalize=None, limiter=None, keep_candidates=False, silence_db, min_silence, tempo_backend, measure_backend)` | Closed loop of synthesize → measure → knob/tempo correction | `engine`: `SpeedEngine`; `cache_dir`: a folder, `True` (default location), `False` (do not use), `None` (default: caches only when the environment variable `TTS_PACE_CACHE_DIR` is set); `target_duration`: total length in seconds including pauses. Give only one of it and `target_sps` (length mode uses `target_sps=None`); giving both, neither, or a value of 0 or less raises `ValueError`; `limiter`: a `Limiter` to cap concurrent runs; `silence_db`, `min_silence`: pause-detection thresholds (same as in `measure`); `tempo_backend`, `measure_backend`: the method used for tempo change and decoding (default `auto`) | `FitResult` (`best`, `base`, `candidates`, `route`, `warnings`, `advice`, `cache_hit`, `save_report()`) |
|
|
265
|
+
| `fit_audio(audio, text, target_sps, out=None, *, target_duration=None, tolerance=0.10, tempo_range, silence_db, min_silence, tempo_backend, measure_backend, limiter=None, lang="auto", normalize=None)` | Fits an existing recording with the tempo alone, with no engine | Same | `FitResult` |
|
|
266
|
+
| `MockEngine(preset="en")` | Practice mock engine (`"en"`/`"ko"`; `base_sps`, `elasticity`, `silent_floor`, `jitter`, `seed` are adjustable) | | Engine |
|
|
267
|
+
| `CallableEngine(fn, *, speed_range=(0.5, 2.0), deterministic=False, cache_id=None, name="callable", has_speed_knob=True)` | Wraps a single Python function as an engine | `fn(text, speed)` returns one of `AudioClip`, a WAV path, or `(samples, sample_rate)` | Engine |
|
|
268
|
+
| `SayEngine(voice=None, *, base_wpm=175, speed_range=(0.86, 1.6))` | Calls macOS `say` | macOS only | Engine |
|
|
269
|
+
| `PiperEngine(voice=None, *, speed_range=(0.6, 1.7), sentence_silence=0.8)` | Calls Piper (runs the `piper` the user installed as a separate process) | `voice`: an `.onnx` path or `TTS_PACE_PIPER_VOICE` | Engine |
|
|
270
|
+
| `Limiter(max_concurrent=2, max_waiting=8)` | Limits the number of concurrent synthesis, tempo and ffmpeg runs | If there is no slot and the queue is full, `BusyError` | Context manager `slot()` |
|
|
271
|
+
| `AudioClip`, `Measurement`, `SyllableCount`, `Candidate`, `FitResult`, `EngineInfo`, `SpeedEngine` | Data types and the engine protocol | | |
|
|
272
|
+
| Exceptions | `TtsPaceError` (common parent), `EngineError`, `AudioFormatError`, `MeasurementError`, `UnsupportedTextError`, `BusyError`. Invalid arguments raise `ValueError` | | |
|
|
273
|
+
| Warning | `UnsupportedCharsWarning` (a warning class emitted through Python's `warnings` module, when there are characters that cannot be counted) | | |
|
|
274
|
+
|
|
275
|
+
Warnings returned by `fit()`: `knob_response_weak` (the knob does not respond as expected), `knob_clamped` (the requested speed was outside the engine's range and was cut), `tempo_clamped` (the tempo limit was hit), `unsupported_chars` (there were characters that could not be counted).
|
|
276
|
+
|
|
277
|
+
Command line: `tts-pace {demo,count,measure,retime,fit}`. See each command's options with `uv run tts-pace <command> --help`.
|
|
278
|
+
|
|
279
|
+
## Usage examples
|
|
280
|
+
|
|
281
|
+
### 1. Measuring and fitting the speed of an existing recording
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
from tts_pace import count_syllables, measure, fit_audio
|
|
285
|
+
|
|
286
|
+
text = "Today we learn how to match the speaking rate of a synthetic voice. It is a short lesson about pace and silence."
|
|
287
|
+
n = count_syllables(text)
|
|
288
|
+
print("syllables:", n.total, n.by_script)
|
|
289
|
+
|
|
290
|
+
m = measure("tts-pace-demo/en_far_base.wav", text=text)
|
|
291
|
+
print(f"total {m.total_s:.2f}s, silence {m.silence_s:.2f}s, speech {m.net_s:.2f}s -> {m.sps:.2f} sps")
|
|
292
|
+
|
|
293
|
+
r = fit_audio("tts-pace-demo/en_far_base.wav", text, target_sps=4.2, out="slower.wav")
|
|
294
|
+
print(r.route, f"{r.best.measurement.sps:.2f} sps", f"err {r.best.err_pct:.2f}%", r.warnings)
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
```text
|
|
298
|
+
syllables: 29 {'ko': 0, 'en': 29, 'ja': 0}
|
|
299
|
+
total 8.00s, silence 1.71s, speech 6.30s -> 4.61 sps
|
|
300
|
+
tempo-only 4.19 sps err 0.14% ()
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
(`tts-pace-demo/en_far_base.wav` is created by `uv run tts-pace demo`.)
|
|
304
|
+
|
|
305
|
+
### 2. Connecting the TTS you use
|
|
306
|
+
|
|
307
|
+
`CallableEngine` needs only a single `fn(text, speed)` function. The `my_tts` below is a placeholder that makes fake sound for illustration. In practice you would call a cloud API or a local model here, and convert `speed` (1.0 = default, larger is faster) into that engine's own unit.
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
import numpy as np
|
|
311
|
+
from tts_pace import CallableEngine, fit
|
|
312
|
+
|
|
313
|
+
def my_tts(text: str, speed: float):
|
|
314
|
+
# Stand-in for a real TTS call: one 0.1 s tone per character; faster speed -> shorter tones.
|
|
315
|
+
sr = 16000
|
|
316
|
+
tone = np.sin(2 * np.pi * 220 * np.arange(int(sr * 0.1 / speed)) / sr) * 0.5
|
|
317
|
+
gap = np.zeros(int(sr * 0.02 / speed))
|
|
318
|
+
return np.concatenate([np.concatenate([tone, gap]) for _ in text]).astype(np.float32), sr
|
|
319
|
+
|
|
320
|
+
engine = CallableEngine(my_tts, speed_range=(0.5, 2.0), name="my-tts")
|
|
321
|
+
r = fit("Hello there friend", target_sps=3.0, engine=engine, out="mine.wav")
|
|
322
|
+
print(r.route, f"{r.best.measurement.sps:.2f} sps", [round(c.speed, 2) for c in r.candidates])
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
```text
|
|
326
|
+
resynth 3.00 sps [1.0, 1.62]
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
What it means: it measured the first synthesis (speed 1.0), resynthesized once at speed 1.62, and hit 3.00 sps. If your engine always produces the same sound for the same input, give `deterministic=True` and `cache_id="my-engine:voice"` to use the result cache.
|
|
330
|
+
|
|
331
|
+
### 3. Real engines (macOS `say`, Piper)
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
from tts_pace import SayEngine, fit
|
|
335
|
+
r = fit("오늘은 속도를 맞춥니다.", target_sps=4.5, engine=SayEngine("Yuna"), out="say.wav")
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
(The text means "Today we match the speed.")
|
|
339
|
+
|
|
340
|
+
> When this README was written, `SayEngine` and `PiperEngine` had **not been verified with real speech.** Command construction and error handling were tested, but the tests that call real engines (`pytest -m real`) are excluded from the default run. Real engines are shared resources and can be slow, and `say -r` was observed not to respond below about 150 wpm, so the default range was limited to 0.86-1.6x.
|
|
341
|
+
|
|
342
|
+
## When to use it
|
|
343
|
+
|
|
344
|
+
- **When tts-pace fits**: when you must fit a target pace in "syllables per second with pauses removed", as in dubbing or narration (a target total length can be given with `--target-duration`), or when you want to measure the speed of an existing recording and adjust only the tempo (0.85-1.15x) slightly. Installation takes only numpy.
|
|
345
|
+
- **When another tool is better**: if you only need to speed things up by a fixed ratio, the TTS engine's own speed option or [ffmpeg `atempo`](https://ffmpeg.org/ffmpeg-filters.html#atempo) is enough. If you need speech synthesis itself, use a TTS engine such as [Piper](https://github.com/OHF-Voice/piper1-gpl).
|
|
346
|
+
- tts-pace is a measurement and correction layer that does not synthesize speech itself.
|
|
347
|
+
|
|
348
|
+
## What we learned
|
|
349
|
+
|
|
350
|
+
These are values measured directly. Measured on 2026-11-09, with piper-tts 1.8.0 (English) and the macOS `say` voice Yuna (Korean). These values are the basis of the defaults.
|
|
351
|
+
|
|
352
|
+
| Item | Measured value | So tts-pace |
|
|
353
|
+
|---|---|---|
|
|
354
|
+
| Proportionality of Piper `length_scale` | At noise 0: 0.7→6.49, 0.85→5.24, 1.0→4.88, 1.2→4.43, 1.5→3.59 sps. From 1.0 to 1.2 it changes only about 55% of what was expected (1/1.2x = 4.07 sps) | Does not stop after one knob adjustment; it measures again and closes the rest with the tempo. `MockEngine("en")`'s `elasticity=0.6` (speed^0.6) imitates this non-linearity |
|
|
355
|
+
| `say -r` floor | For Yuna, `-r 120` and `-r 150` give the same result (7.531 seconds, 4.91 sps). 175→220→280 is nearly proportional (5.40→6.68→8.13 sps) | Limits `SayEngine`'s default range to 0.86-1.6x and raises `knob_response_weak` when the knob does not respond. `MockEngine("ko")`'s `silent_floor=0.9` imitates this "quiet floor" |
|
|
356
|
+
| Non-determinism of Piper defaults | Synthesizing the same sentence twice gives different file hashes and lengths (2.566 s / 2.601 s). With `--noise-scale 0 --noise-w-scale 0` they are the same | `PiperEngine` fixes noise at 0 to protect caching and reproducibility (the base speaking rate also changes from about 4.6 to 4.88 sps) |
|
|
357
|
+
| Accuracy of English syllable counting | On 3,000 random CMUdict words, the `syllables` package scores 78.47% (measured in the preliminary survey). This library's rules score 89.6% on a sample of 1,994 words from the same dictionary (test `tests/test_count_en_accuracy.py`); the samples differ, so it is not a strict comparison | States in [Limitations](#limitations) that English is an estimate |
|
|
358
|
+
| Pause detection | The pauses between sentences from Piper `--sentence-silence 0.8` (about 0.85-0.99 seconds) are detected and excluded. `say` Korean has pauses shorter than 0.6 seconds, so there are 0 | Sets the pause threshold at 0.6 seconds |
|
|
359
|
+
|
|
360
|
+
Results of fitting with real engines (error against the target): English Piper at a 4.0 sps target reached 0.17% with resynthesis + tempo; Korean `say` at a 5.0 sps target reached 0.16% with the tempo alone; Korean `say` at a 4.0 sps target hit the 0.85x tempo floor and ended at 4.5%. These figures were obtained by running real Piper and `say` directly during the survey before the design. They were not re-verified with the finished `SayEngine` and `PiperEngine` adapters, and as noted in [Usage example 3](#3-real-engines-macos-say-piper), they are not in the default tests either.
|
|
361
|
+
|
|
362
|
+
## Limitations
|
|
363
|
+
|
|
364
|
+
- English syllables are a rule-based estimate (89.6% word-level agreement on a sample of 1,994 CMUdict words). Korean is counted by character block and Japanese by mora.
|
|
365
|
+
- The tempo change is limited to 0.85-1.15x. Differences beyond that must be solved with the knob or the sentence length.
|
|
366
|
+
- The length for `--target-duration` is the total length including pauses, and it is hit exactly only when the required tempo is within 0.85-1.15x. Otherwise it stops nearby with a warning.
|
|
367
|
+
- Known limitations (as of 0.3.0): giving `--target-duration` a value of 0 or less prints the mock-engine notice line after the `error:` line (the exit code is still the correct 2). Some of the length-mode tests have weak value-unit checks and will be strengthened later. The `--json` tests do not yet cover the combinations of `--lang`, and `--target-duration` with `--cache-dir`. When `--json` hits an error (in mock mode), the first line of standard error is the `[mock]` notice and the `error:` line comes after it (standard output is empty and the exit code is 2).
|
|
368
|
+
- The mock engine is for practicing the measurement and correction logic and does not represent the quality or speed characteristics of real speech.
|
|
369
|
+
|
|
370
|
+
## Versions and compatibility
|
|
371
|
+
|
|
372
|
+
- Follows Semantic Versioning (SemVer). During 0.x the API may change in minor versions, and any change is recorded in [CHANGELOG.md](https://github.com/LeeGyeongPil/tts-pace/blob/v0.3.0/CHANGELOG.md).
|
|
373
|
+
- Python 3.12 or later.
|
|
374
|
+
|
|
375
|
+
## Troubleshooting
|
|
376
|
+
|
|
377
|
+
| Symptom | Cause | Fix |
|
|
378
|
+
|---|---|---|
|
|
379
|
+
| `knob_response_weak` warning | The engine's speed knob stops at its floor or ceiling, or is non-linear | Use a different voice or engine setting, or edit the sentence |
|
|
380
|
+
| `tempo_clamped` warning | The tempo needed to reach the target (speed or `--target-duration` length) is outside 0.85-1.15x | Adjust the sentence length or the target speed or length |
|
|
381
|
+
| `No countable syllables found` error | There are only symbols or only unsupported characters, so nothing can be counted | Tell it how the text is read with `normalize={"2026": "twenty twenty six"}` |
|
|
382
|
+
| `SayEngine only works on macOS` | `say` is macOS only | Use `CallableEngine` or `PiperEngine` |
|
|
383
|
+
| `PiperEngine needs a voice .onnx file` | No voice file was specified | Set `voice=...` or `TTS_PACE_PIPER_VOICE` |
|
|
384
|
+
|
|
385
|
+
## Development and contributing
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
uv sync
|
|
389
|
+
uv run pytest -q # 99 passed, 2 deselected (tests that need a real engine are excluded by default)
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## Learn more
|
|
393
|
+
|
|
394
|
+
- Piper TTS: https://github.com/OHF-Voice/piper1-gpl (checked 2026-11-09)
|
|
395
|
+
- A discussion of adjusting Piper's speaking rate: https://community.home-assistant.io/t/how-to-set-piper-speaking-rate/662014 (checked 2026-11-09)
|
|
396
|
+
|
|
397
|
+
## License
|
|
398
|
+
|
|
399
|
+
MIT. Piper is GPL-licensed, so it is not included in this package; the `piper` the user installed is called as a separate process.
|
|
400
|
+
|
|
401
|
+
`tests/fixtures/cmudict_sample.tsv` is a sample of the CMU Pronouncing Dictionary (BSD 2-clause, `tests/fixtures/CMUDICT-LICENSE`).
|