local-piper-tts-multilang-secure
Local offline text-to-speech via Piper TTS. Self-contained setup, automatic language detection, per-call voice selection. Extensible to any language. Writes output into the OpenClaw workspace.
安装 / 下载方式
TotalClaw CLI推荐
totalclaw install clawskills:clawskills~szafranski-local-piper-tts-multilang-securecURL直接下载,无需登录
curl -fsSL https://skills.taituai.com/api/skills/clawskills%3Aclawskills~szafranski-local-piper-tts-multilang-secure/file -o szafranski-local-piper-tts-multilang-secure.md# local-piper-tts-multilang-secure
## Description
Local (offline) text-to-speech via Piper.
**Purpose:** generate audio files (OGG/Opus by default) from text, fully offline.
**No sending** is performed by the skill — sending is handled by the agent after the file is ready.
## Features
- Fully offline (no API keys)
- Self-contained setup via `setup()` — installs Piper into an isolated venv, no system-wide changes
- Automatic language detection for 20+ languages with English as default
- Per-call voice selection via `voice` parameter
- On-demand voice download via `downloadVoices()` — no models bundled, choose what you need
- Voice removal via `removeVoice()` — clean up voices you no longer want
- Extensible: add any language by installing a Piper `.onnx` model
- Writes outputs into OpenClaw workspace
## First-run flow — full agent procedure
Follow this sequence exactly when the user asks to use TTS for the first time in a setup context.
### Step 1 — check status
```js
const s = await status();
```
### Step 2 — install Piper if needed
If `s.stage` is `not-setup` or `no-piper`:
- Tell the user: *"To use local TTS I need to install piper-tts into the skill's venv (~30 seconds, one-time). OK to proceed?"*
- Wait for confirmation, then call `setup()`.
- If setup returns a step containing "WARNING: espeak-ng not found", relay the warning and install instructions to the user.
- Call `status()` again after setup completes.
### Step 3 — offer voice download if no models present
If `s.stage` is `no-model` (Piper installed but no `.onnx` files):
**3a. Offer English defaults:**
Explain that two English voices are available as defaults (~65 MB each):
- `en_US-ryan-medium` — male, American
- `en_US-amy-medium` — female, American
Ask which they want, or both: *"Which English voice(s) should I download? Ryan (male), Amy (female), or both?"*
**3b. Ask about other languages:**
After the English choice, ask: *"Do you need any other languages? For example German, French, Spanish, Polish, Italian, Portuguese, Russian… Just tell me and I'll check what's available."*
If the user names a language, look up the available models at https://github.com/rhasspy/piper/blob/master/VOICES.md and list the options. Download whatever the user picks using the same `downloadVoices()` call.
**3c. Download everything at once:**
```js
const result = await downloadVoices(['en_US-ryan-medium', 'en_US-amy-medium', /* + any others */]);
// result.downloaded — succeeded
// result.failed — [{stem, error}] if any failed
```
Each voice requires internet access. Download takes ~1–2 min per voice on a typical connection.
If any downloads fail:
- Check internet connectivity
- Verify the stem exists at https://github.com/rhasspy/piper/blob/master/VOICES.md
- Offer to retry
### Step 4 — play samples so the user can choose
After downloading, generate a short audio sample for each downloaded voice and send it to the user.
For each voice, use a greeting **in the voice's language**:
- English: `"Hello, I'm [name]. How can I help you today?"`
- German: `"Hallo, ich heiße [Name]. Wie kann ich Ihnen helfen?"`
- French: `"Bonjour, je m'appelle [prénom]. Comment puis-je vous aider?"`
- Spanish: `"Hola, me llamo [nombre]. ¿Cómo puedo ayudarte?"`
- Polish: `"Cześć, mam na imię [imię]. Jak mogę Ci pomóc?"`
- Italian: `"Ciao, mi chiamo [nome]. Come posso aiutarti?"`
- Portuguese: `"Olá, meu nome é [nome]. Como posso ajudar?"`
- Russian: `"Привет, меня зовут [имя]. Чем могу помочь?"`
- For other languages: use an equivalent native greeting.
Replace `[name]` with the voice name (e.g. *Ryan*, *Amy*, *Thorsten*).
```js
const sample = await tts({ text: 'Hello, I\'m Ryan. How can I help you today?', voice: 'en_US-ryan-medium' });
// send sample.path to the user as a voice message
```
Send all samples, then ask: *"Which voice do you prefer? Or shall I download a different one?"*
### Step 5 — choose speech speed
After the user picks a voice, ask:
*"How fast should I speak? Normal is 100%. Some options: 125% (faster), 115% (slightly faster), 100% (normal), 80% (slower) — or tell me a percentage."*
Always present speed as a percentage to the user. Never mention `lengthScale` directly.
`lengthScale` is the internal duration multiplier — lower = faster. To convert: `lengthScale = 1 / (speed% / 100)`.
Examples:
- 125% speed → lengthScale 0.8
- 115% speed → lengthScale 0.87
- 100% speed → lengthScale 1.0 (default)
- 80% speed → lengthScale 1.25
Generate a short sample at the chosen speed so the user can hear the difference:
```js
const sample = await tts({ text: 'This is how I sound at this speed.', voice: 'chosen-voice', lengthScale: 0.8 });
// send sample.path to the user
```
Confirm with the user, then offer to save it permanently:
*"Should I save this as your default speed? It'll be used automatically every session."*
If the user agrees:
```js
await saveConfig({ lengthScale: 0.8 });
```
Once saved, `tts()` reads it from `config.json` in the skill directory automatically — no need to pass `lengthScale` on every call.
### Step 6 — note the preferred voice and speed
Once confirmed, remember both `voice` and `lengthScale` for the session. Pass them to every subsequent `tts()` call unless the user asks to change them.
---
## Before first use — always call status()
**Always call `status()` before the first `tts()` call in a session** to determine what is needed.
| `stage` | Meaning | What to do |
|---|---|---|
| `ready` | Fully installed, at least one voice model present | Proceed with `tts()` |
| `not-setup` | Piper not installed | Ask user for confirmation, then call `setup()` |
| `no-piper` | Venv exists but piper binary missing | Ask user for confirmation, then call `setup()` |
| `no-model` | Piper installed but no voice model downloaded | Follow Steps 3–5 of first-run flow above |
**IMPORTANT: Always ask the user for confirmation before calling `setup()`.**
It installs the `piper-tts` package from PyPI into a venv inside the skill directory.
## Usage
- Input: `text`, optional `format` (`"ogg"` or `"wav"`), optional `voice` (model stem), optional `lengthScale` (speech speed, default `1.0`)
- Output: path to generated file (usually `.ogg`)
## Controlling voice and language
**To list installed voices**, call `listVoices()` — returns stems of all installed `.onnx` models.
Never assume a fixed list; it varies per user and installation.
**Auto-detection (no `voice` param):**
The script detects language from the text using character and script analysis:
- Non-Latin scripts: Cyrillic (Russian, Ukrainian, Bulgarian), Greek, Arabic, Persian, Chinese, Japanese, Korean, Georgian
- Latin-script languages: Vietnamese, Polish, Romanian, Turkish, Czech, Slovak, Hungarian, Portuguese, Spanish, Catalan, German, Finnish, Scandinavian (Swedish, Norwegian, Danish), French, Italian
- Fallback: English keywords → first English model → any installed model
Auto-detection is best-effort. For reliable results with a specific language, always pass the `voice` parameter explicitly.
**Explicit override:** set `PIPER_VOICE_MODEL` env var to a full `.onnx` path (overrides everything).
**When the user requests a specific voice or language:**
1. Call `listVoices()` to see what is installed
2. Pass the matching stem as `voice` to `tts()`, e.g. `voice: "en_US-amy-medium"`
3. If the requested voice is not installed, offer to download it with `downloadVoices([stem])`
**To switch back to auto-detect**, omit the `voice` parameter.
## Downloading additional voices
The user may say things like *"I don't like this voice, use a female one"* or
*"Download a German voice"*. When this happens:
1. Find the model at https://github.com/rhasspy/piper/blob/master/VOICES.md
2. Confirm the stem (e.g. `de_DE-thorsten-medium`) and call `downloadVoices([stem])`
3. Generate a sample and send it to the user
4. Confirm with `listVoices()` — the new voice is immediately usable
## Removing voices
The user may say *"remove that voice"* or