feat: add CW-026 Local Voice plugin
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "local-voice",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Generate validated local speech with reusable CosyVoice profiles.",
|
||||||
|
"author": {
|
||||||
|
"name": "Bryan Gilliom / Message Point Media"
|
||||||
|
},
|
||||||
|
"repository": "https://git.mpm.to/mpm/local-voice",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"keywords": [
|
||||||
|
"tts",
|
||||||
|
"voice",
|
||||||
|
"cosyvoice",
|
||||||
|
"briefing",
|
||||||
|
"local-ai"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "local-voice",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Generate validated local speech with reusable CosyVoice profiles.",
|
||||||
|
"author": {
|
||||||
|
"name": "Bryan Gilliom / Message Point Media"
|
||||||
|
},
|
||||||
|
"skills": "./skills/",
|
||||||
|
"interface": {
|
||||||
|
"displayName": "Local Voice",
|
||||||
|
"shortDescription": "Generate local briefing and notification audio.",
|
||||||
|
"longDescription": "Local Voice plans, renders, validates, and assembles reusable CosyVoice profiles on local hardware.",
|
||||||
|
"developerName": "Message Point Media",
|
||||||
|
"category": "Productivity",
|
||||||
|
"capabilities": [
|
||||||
|
"local-text-to-speech",
|
||||||
|
"voice-profiles",
|
||||||
|
"audio-validation"
|
||||||
|
],
|
||||||
|
"defaultPrompt": [
|
||||||
|
"Generate this script with Local Voice and validate the finished audio.",
|
||||||
|
"List the locally installed voice profiles.",
|
||||||
|
"Diagnose my Local Voice runtime."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
.DS_Store
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.venv/
|
||||||
|
runtime/
|
||||||
|
generated/
|
||||||
|
outputs/
|
||||||
|
models/
|
||||||
|
voice-assets/
|
||||||
|
*.wav
|
||||||
|
*.mp3
|
||||||
|
*.m4a
|
||||||
|
*.flac
|
||||||
|
*.onnx
|
||||||
|
*.safetensors
|
||||||
|
*.plugin
|
||||||
|
*.zip
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 0.1.0 — 2026-07-27
|
||||||
|
|
||||||
|
- Added a shared Claude CoWork and Codex Local Voice skill.
|
||||||
|
- Added unified planning, Metal/CPU/CUDA rendering, Whisper alignment, cadence
|
||||||
|
correction, resumable assembly, fixed assets, and transcript QA.
|
||||||
|
- Added Donna, Chris Engineer, Grandpa Bomber, Ryan Pitch Meeting, and Val
|
||||||
|
Holiday profiles.
|
||||||
|
- Added verified macOS installation and recovery instructions.
|
||||||
|
- Added provisional Windows/NVIDIA deployment guidance.
|
||||||
|
- Added Donna dependency contract and fresh acceptance cases.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
Copyright 2026 Message Point Media
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
# Local Voice
|
||||||
|
|
||||||
|
Local, validated CosyVoice3 speech generation for reusable MPM briefing and
|
||||||
|
character profiles.
|
||||||
|
|
||||||
|
**Version:** 0.1.0
|
||||||
|
**Author:** Bryan Gilliom / Message Point Media
|
||||||
|
**Repository:** https://git.mpm.to/mpm/local-voice
|
||||||
|
**CoWork Project:** CW-026 — Local Voice
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Local Voice is a shared audio dependency for Claude CoWork, Codex, and other
|
||||||
|
local automation. It converts a canonical script or structured two-role
|
||||||
|
dialogue into WAV or MP3, retaining raw generations and Whisper alignment data
|
||||||
|
so failed seams can be repaired without regenerating successful speech. Voice
|
||||||
|
reference audio is installed separately from the public plugin and remains in
|
||||||
|
the private CW-026 recovery package.
|
||||||
|
|
||||||
|
The verified production configuration is Apple Silicon Metal with the
|
||||||
|
CosyVoice3 Candle model. A provisional NVIDIA/CUDA path is documented for the
|
||||||
|
planned Windows host but is not yet certified.
|
||||||
|
|
||||||
|
## Skill
|
||||||
|
|
||||||
|
| Skill | What it does |
|
||||||
|
|---|---|
|
||||||
|
| `local-voice` | Plans, renders, resumes, aligns, validates, and assembles local speech when a user asks for a voice brief, local TTS message, spoken notification, character dialogue, CosyVoice render, or MP3 briefing. |
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `list` | List bundled voice profiles and their roles. |
|
||||||
|
| `doctor` | Verify Python, audio tools, model files, and authorized assets. |
|
||||||
|
| `plan` | Convert simple Markdown or Ryan-labelled dialogue into a render plan. |
|
||||||
|
| `validate` | Validate a hand-authored render plan without generating audio. |
|
||||||
|
| `render` | Generate, align, process, assemble, and quality-check audio. |
|
||||||
|
|
||||||
|
`render --resume` reuses completed raw generations. `render --assemble-only`
|
||||||
|
repairs trimming or seams without rerunning the model.
|
||||||
|
|
||||||
|
## Supported profiles
|
||||||
|
|
||||||
|
- Donna
|
||||||
|
- Chris Engineer
|
||||||
|
- Grandpa Bomber
|
||||||
|
- Ryan Pitch Meeting: Producer and Writer roles
|
||||||
|
- Val Holiday
|
||||||
|
|
||||||
|
See `skills/local-voice/references/voice-catalog.md` for the production rules
|
||||||
|
that distinguish these profiles.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Follow `docs/INSTALL_MACOS.md` on the verified Apple Silicon path.
|
||||||
|
2. Install the private `local-voice-authorized-assets` archive from CW-026.
|
||||||
|
3. Download the exact Candle model into the runtime `models` directory.
|
||||||
|
4. Run `scripts/verify_install.py`.
|
||||||
|
5. Install the Claude or Codex plugin package.
|
||||||
|
6. Start a new task so the host discovers the skill.
|
||||||
|
|
||||||
|
The private recovery package and public plugin are intentionally separate:
|
||||||
|
the public repository contains redistributable software, while the private
|
||||||
|
Drive folder contains the authorized voice configuration.
|
||||||
|
|
||||||
|
## Calling Local Voice
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/local_voice.py plan \
|
||||||
|
--voice donna \
|
||||||
|
--script /absolute/path/brief.md \
|
||||||
|
--output-plan /absolute/path/brief-plan.json \
|
||||||
|
--output-audio /absolute/path/brief.mp3
|
||||||
|
|
||||||
|
python3 scripts/local_voice.py render \
|
||||||
|
/absolute/path/brief-plan.json \
|
||||||
|
--resume
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `LOCAL_VOICE_RUNTIME` when the runtime is not installed at the
|
||||||
|
platform-default location.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.11 recommended
|
||||||
|
- CosyVoice3 0.1.0 Candle build
|
||||||
|
- CosyVoice3-0.5B-Candle model
|
||||||
|
- ffmpeg
|
||||||
|
- Whisper CLI with word timestamps
|
||||||
|
- NumPy and SoundFile
|
||||||
|
- Authorized reference audio and transcripts
|
||||||
|
|
||||||
|
## Operational rules
|
||||||
|
|
||||||
|
- Keep canonical `text` separate from pronunciation-safe `tts_text`.
|
||||||
|
- Generate the largest safe complete passages; paragraph breaks are candidates,
|
||||||
|
not mandatory cuts.
|
||||||
|
- Do not normalize tempo on short greetings, closings, or reactions.
|
||||||
|
- Preserve raw WAV, Whisper JSON, processed WAV, and QA reports.
|
||||||
|
- Use fixed assets for exact Ryan signatures and very short reactions.
|
||||||
|
- Do not silently fall back to a cloud provider.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Resolution |
|
||||||
|
|---|---|
|
||||||
|
| A word is clipped | Retain more natural tail or add neighboring sacrificial context, then rerun with `--assemble-only` when possible. |
|
||||||
|
| A seam clicks | Move the join to a quiet boundary or add a natural pause; do not regenerate speech that already passed. |
|
||||||
|
| A short reaction sounds generic | Use an authorized fixed asset from the Ryan signature library. |
|
||||||
|
| Opening or closing sounds drunk or rushed | Remove tempo adjustment; short frames stay native. |
|
||||||
|
| Literal alignment fails | Inspect retained Whisper JSON; normalized fuzzy alignment is expected to tolerate ordinary ASR variation. |
|
||||||
|
| `lead` uses the wrong pronunciation | Keep canonical text and use a local `tts_text` override such as `led` for ammunition. |
|
||||||
|
|
||||||
|
## Security and rights
|
||||||
|
|
||||||
|
Local Voice does not require a service credential. Reference audio must only be
|
||||||
|
installed or used when the operator has authorization. The public repository
|
||||||
|
must never contain private voice assets, source recordings, generated briefings,
|
||||||
|
or model binaries.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Acceptance results
|
||||||
|
|
||||||
|
Fresh acceptance audio was generated on 2026-07-27 using Apple Silicon Metal,
|
||||||
|
the production Candle model, the private authorized assets, and the unified
|
||||||
|
renderer in Local Voice 0.1.0.
|
||||||
|
|
||||||
|
| Profile | Duration | Transcript coverage | Transcript precision | Click-risk seams |
|
||||||
|
|---|---:|---:|---:|---:|
|
||||||
|
| Donna | 17.49 s | 0.978 | 0.957 | 0 |
|
||||||
|
| Chris Engineer | 14.73 s | 0.960 | 0.923 | 0 |
|
||||||
|
| Grandpa Bomber | 21.79 s | 0.934 | 0.934 | 0 |
|
||||||
|
| Val Holiday | 23.45 s | 0.964 | 0.946 | 0 |
|
||||||
|
| Ryan Pitch Meeting | 16.62 s | 0.963 | 0.981 | 0 |
|
||||||
|
|
||||||
|
## What the suite verifies
|
||||||
|
|
||||||
|
- Every profile and both Ryan roles load from the private runtime.
|
||||||
|
- Generated targets contain their expected beginning and final-word marker.
|
||||||
|
- Final transcript coverage is at least 0.60.
|
||||||
|
- Final transcript precision is at least 0.75, preventing sacrificial context
|
||||||
|
from silently leaking into the finished audio.
|
||||||
|
- Every join has a boundary step below the click-risk threshold.
|
||||||
|
- Short frames remain at native tempo.
|
||||||
|
- Val normalizes only sufficiently long substantive passages toward 170 WPM.
|
||||||
|
- Chris uses 30-word safe blocks after a 42-word block proved vulnerable to
|
||||||
|
model truncation.
|
||||||
|
- Ryan uses fixed `Yes sir, I do`, `is tight`, and
|
||||||
|
`super easy, barely an inconvenience / Oh really` assets in the acceptance
|
||||||
|
dialogue.
|
||||||
|
|
||||||
|
## Human review
|
||||||
|
|
||||||
|
Objective acceptance cannot judge character similarity or comic timing.
|
||||||
|
The packaged acceptance MP3s are retained for listening review. Future changes
|
||||||
|
to reference audio, cadence thresholds, trimming, or asset selection should be
|
||||||
|
compared against these files before release.
|
||||||
|
|
||||||
|
## Latest jobs
|
||||||
|
|
||||||
|
| Profile | Job ID |
|
||||||
|
|---|---|
|
||||||
|
| Donna | `acceptance-v2-donna` |
|
||||||
|
| Chris Engineer | `acceptance-v6-chris-engineer` |
|
||||||
|
| Grandpa Bomber | `acceptance-v7-grandpa-bomber` |
|
||||||
|
| Val Holiday | `acceptance-v7-val-holiday` |
|
||||||
|
| Ryan Pitch Meeting | `acceptance-v9-ryan-pitch-meeting` |
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Donna integration contract
|
||||||
|
|
||||||
|
Donna remains responsible for collecting organizational information, deciding
|
||||||
|
priority, selecting a character, and writing the final canonical script. Local
|
||||||
|
Voice is an optional audio dependency responsible for rendering and validating
|
||||||
|
that script.
|
||||||
|
|
||||||
|
## Invocation
|
||||||
|
|
||||||
|
Donna supplies:
|
||||||
|
|
||||||
|
- voice profile ID;
|
||||||
|
- canonical Markdown or Ryan-labelled dialogue;
|
||||||
|
- absolute output path;
|
||||||
|
- pronunciation overrides when needed;
|
||||||
|
- optional delivery, role, signature-asset, and pause metadata.
|
||||||
|
|
||||||
|
Donna then asks Local Voice to:
|
||||||
|
|
||||||
|
1. run `doctor` if runtime health is unknown;
|
||||||
|
2. build or validate the render plan;
|
||||||
|
3. render with `--resume`;
|
||||||
|
4. return the final audio and QA-report paths.
|
||||||
|
|
||||||
|
## Required behavior
|
||||||
|
|
||||||
|
- Do not send Markdown headings, numbered section titles, speaker labels, or
|
||||||
|
stage directions as spoken text.
|
||||||
|
- Keep greetings and closings as short standalone paragraphs so Local Voice can
|
||||||
|
preserve native tempo.
|
||||||
|
- Keep substantive paragraphs to complete sentence groups of roughly 20–42
|
||||||
|
words unless a profile specifies otherwise.
|
||||||
|
- Use canonical `text` for the written brief and `tts_text` only for local
|
||||||
|
pronunciation corrections.
|
||||||
|
- Do not silently fall back to ElevenLabs. If Local Voice fails, Donna should
|
||||||
|
report the failure and apply the caller's configured fallback policy.
|
||||||
|
- Attach or link the final audio alongside the full written organizational
|
||||||
|
brief; the audio is a quick summary, not the sole record.
|
||||||
|
|
||||||
|
## Character mapping
|
||||||
|
|
||||||
|
| Donna character | Local Voice profile |
|
||||||
|
|---|---|
|
||||||
|
| Production Donna | `donna` |
|
||||||
|
| Chris Engineer | `chris-engineer` |
|
||||||
|
| Grandpa Bomber | `grandpa-bomber` |
|
||||||
|
| Pitch Meeting | `ryan-pitch-meeting` |
|
||||||
|
| Val Holiday | `val-holiday` |
|
||||||
|
|
||||||
|
## Example handoff
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"voice": "val-holiday",
|
||||||
|
"script": "/absolute/path/val-brief.md",
|
||||||
|
"output": "/absolute/path/val-brief.mp3",
|
||||||
|
"fallback": "report-and-return-text"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Local Voice returns the final audio path, QA report path, duration, and
|
||||||
|
segment-level completeness and cadence results.
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# Verified macOS installation
|
||||||
|
|
||||||
|
This procedure recreates the production configuration verified on an Apple
|
||||||
|
Silicon MacBook Pro. It installs the software and model separately from the
|
||||||
|
private authorized voice-asset archive.
|
||||||
|
|
||||||
|
## Verified configuration
|
||||||
|
|
||||||
|
- Apple Silicon macOS
|
||||||
|
- Python 3.11
|
||||||
|
- CosyVoice3 `0.1.0+metal`
|
||||||
|
- 24 kHz Candle model: `spensercai/CosyVoice3-0.5B-Candle`
|
||||||
|
- ffmpeg and Whisper available on `PATH`
|
||||||
|
- Metal inference with full-precision weights
|
||||||
|
|
||||||
|
## 1. Install system prerequisites
|
||||||
|
|
||||||
|
Install Homebrew if it is not already present, then install:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install python@3.11 ffmpeg
|
||||||
|
```
|
||||||
|
|
||||||
|
Install the Whisper CLI into an isolated environment or with `pipx`. Confirm:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ffmpeg -version
|
||||||
|
whisper --help
|
||||||
|
python3.11 --version
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Create the runtime
|
||||||
|
|
||||||
|
The default runtime is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/Library/Application Support/MPM Local Voice/runtime
|
||||||
|
```
|
||||||
|
|
||||||
|
Create its `models`, `voices`, `jobs`, and `wheels` directories. Create a Python
|
||||||
|
3.11 virtual environment beside or inside the runtime and activate it.
|
||||||
|
|
||||||
|
For a guided installation after downloading the model and private packages:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zsh scripts/install_macos.sh \
|
||||||
|
--wheel /path/to/cosyvoice3-0.1.0+metal-cp310-abi3-macosx_11_0_arm64.whl \
|
||||||
|
--assets /path/to/local-voice-authorized-assets-v0.1.0.tar.gz \
|
||||||
|
--model-dir /path/to/CosyVoice3-0.5B-Candle
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Install the tested Metal wheel
|
||||||
|
|
||||||
|
Copy this file from the private CW-026 recovery package:
|
||||||
|
|
||||||
|
```text
|
||||||
|
cosyvoice3-0.1.0+metal-cp310-abi3-macosx_11_0_arm64.whl
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected SHA-256:
|
||||||
|
|
||||||
|
```text
|
||||||
|
c9c04352fe0e559b7b43129baae7035b25b48922e2296498efc53c39874d4e39
|
||||||
|
```
|
||||||
|
|
||||||
|
Install the wheel and runtime Python dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pip install ./wheels/cosyvoice3-0.1.0+metal-cp310-abi3-macosx_11_0_arm64.whl
|
||||||
|
python -m pip install numpy==2.4.6 soundfile==0.14.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Install the model
|
||||||
|
|
||||||
|
Download `spensercai/CosyVoice3-0.5B-Candle` from Hugging Face into:
|
||||||
|
|
||||||
|
```text
|
||||||
|
runtime/models/CosyVoice3-0.5B-Candle
|
||||||
|
```
|
||||||
|
|
||||||
|
The private recovery manifest contains hashes for the production model files.
|
||||||
|
At minimum the directory must contain:
|
||||||
|
|
||||||
|
- `llm.safetensors`
|
||||||
|
- `flow.safetensors`
|
||||||
|
- `hift.safetensors`
|
||||||
|
- `campplus.onnx`
|
||||||
|
- `speech_tokenizer_v3.onnx`
|
||||||
|
- `config.json`
|
||||||
|
|
||||||
|
The model is approximately 4.7 GB and is not bundled with the public plugin.
|
||||||
|
|
||||||
|
## 5. Install authorized voice assets
|
||||||
|
|
||||||
|
Extract the private archive so the runtime contains:
|
||||||
|
|
||||||
|
```text
|
||||||
|
runtime/voices/donna
|
||||||
|
runtime/voices/chris-engineer
|
||||||
|
runtime/voices/grandpa-bomber
|
||||||
|
runtime/voices/ryan-pitch-meeting
|
||||||
|
runtime/voices/val-holiday
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not publish or redistribute this archive.
|
||||||
|
|
||||||
|
## 6. Verify
|
||||||
|
|
||||||
|
From the plugin root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/local_voice.py doctor --device metal
|
||||||
|
python scripts/verify_install.py --device metal
|
||||||
|
```
|
||||||
|
|
||||||
|
Every dependency, model, and voice profile must report `PASS`.
|
||||||
|
|
||||||
|
## 7. Install the plugin
|
||||||
|
|
||||||
|
For Claude CoWork, install `local-voice-v0.1.0.plugin` from the private CW-026
|
||||||
|
folder. For Codex, install from the MPM marketplace or use the same repository
|
||||||
|
source containing `.codex-plugin/plugin.json`.
|
||||||
|
|
||||||
|
Start a new task after installation so the host loads the skill.
|
||||||
|
|
||||||
|
## Restore policy
|
||||||
|
|
||||||
|
The public repository is authoritative for code and documentation. The private
|
||||||
|
CW-026 folder is authoritative for the tested wheel, authorized voice assets,
|
||||||
|
checksums, plugin packages, and recovery notes. The model is reproducible from
|
||||||
|
its exact source and hashes; an optional offline model archive may be added
|
||||||
|
later.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Recovery and private asset policy
|
||||||
|
|
||||||
|
CW-026 uses a split distribution:
|
||||||
|
|
||||||
|
- **Public Gitea:** redistributable source, profiles, schemas, tests, and docs.
|
||||||
|
- **Private Google Drive:** authorized voice assets, the tested Metal wheel,
|
||||||
|
installable plugin packages, checksums, and recovery manifests.
|
||||||
|
- **External model source:** exact Candle model repository and verified hashes.
|
||||||
|
|
||||||
|
## Private recovery folder
|
||||||
|
|
||||||
|
`CW-026 — Local Voice`
|
||||||
|
|
||||||
|
https://drive.google.com/drive/folders/1bcgkABj-JGavyFHRqSj2gwZqhnPHBAhk
|
||||||
|
|
||||||
|
The private folder is visible only to authorized MPM staff. It is the recovery
|
||||||
|
source of truth for the machine-specific configuration.
|
||||||
|
|
||||||
|
## Restore sequence
|
||||||
|
|
||||||
|
1. Restore the plugin source or installable package.
|
||||||
|
2. Recreate the Python 3.11 environment.
|
||||||
|
3. Install the tested platform wheel.
|
||||||
|
4. Download and verify the exact model.
|
||||||
|
5. Extract the authorized voice-assets archive into the runtime.
|
||||||
|
6. Run `doctor`.
|
||||||
|
7. Run the acceptance suite.
|
||||||
|
8. Configure calling skills, such as Donna, to use the Local Voice dependency.
|
||||||
|
|
||||||
|
Never delete the working runtime during recovery preparation. Copy assets into
|
||||||
|
the private archive and verify the archive before treating it as a backup.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Windows and NVIDIA deployment notes
|
||||||
|
|
||||||
|
This path is a recommendation for the planned dedicated Windows/NVIDIA host.
|
||||||
|
It has not yet passed the Local Voice acceptance suite and must be treated as
|
||||||
|
provisional.
|
||||||
|
|
||||||
|
## Recommended host
|
||||||
|
|
||||||
|
- Windows 11
|
||||||
|
- Current NVIDIA Studio or production driver
|
||||||
|
- Python 3.11 x64
|
||||||
|
- ffmpeg and Whisper available on `PATH`
|
||||||
|
- Sufficient SSD space for the 4.7 GB model, jobs, and retained WAV files
|
||||||
|
- NVIDIA GPU with supported CUDA capability and practical VRAM headroom
|
||||||
|
|
||||||
|
## Preferred deployment order
|
||||||
|
|
||||||
|
1. Start with native Windows and a CosyVoice3 wheel built for the installed CUDA
|
||||||
|
runtime.
|
||||||
|
2. If native dependency resolution is unreliable, use WSL2 with NVIDIA CUDA
|
||||||
|
passthrough and the Linux CUDA build.
|
||||||
|
3. Use `LOCAL_VOICE_RUNTIME` to point at a dedicated data directory, for example
|
||||||
|
`D:\MPM-Local-Voice\runtime`.
|
||||||
|
4. Copy the same private `voices` archive and model directory used on macOS.
|
||||||
|
5. Run `doctor --device cuda`.
|
||||||
|
6. Run the full acceptance suite before scheduling production briefings.
|
||||||
|
|
||||||
|
## Important differences
|
||||||
|
|
||||||
|
- The verified Metal wheel cannot run on Windows.
|
||||||
|
- CUDA, driver, and wheel versions must agree.
|
||||||
|
- Do not assume that a CUDA build exists merely because the NVIDIA driver is
|
||||||
|
installed.
|
||||||
|
- Keep ffmpeg path quoting and Windows long-path behavior in mind.
|
||||||
|
- Compare transcript coverage, ending confidence, seam derivatives, duration,
|
||||||
|
and subjective voice similarity against the macOS acceptance outputs.
|
||||||
|
|
||||||
|
## Fallback
|
||||||
|
|
||||||
|
CPU generation remains functionally possible but may be slower. It is suitable
|
||||||
|
for overnight batches if CUDA setup is delayed, provided the acceptance suite
|
||||||
|
passes on that host.
|
||||||
|
|
||||||
|
## Certification checklist
|
||||||
|
|
||||||
|
- [ ] `cosyvoice3`, NumPy, and SoundFile import
|
||||||
|
- [ ] Candle model hashes match the recovery manifest
|
||||||
|
- [ ] All authorized voice assets resolve
|
||||||
|
- [ ] CUDA device loads successfully
|
||||||
|
- [ ] Every acceptance output passes transcript QA
|
||||||
|
- [ ] Ryan fixed assets and dialogue assembly are seamless
|
||||||
|
- [ ] Five production briefs can complete inside the overnight window
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"skill_name": "local-voice",
|
||||||
|
"evals": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"prompt": "Generate this three-paragraph operational update with Donna using local TTS, preserve the short closing at native tempo, and return the MP3 and QA report.",
|
||||||
|
"expected_output": "A complete Donna MP3 with passing transcript QA, retained raw and alignment artifacts, and no cloud fallback.",
|
||||||
|
"files": [
|
||||||
|
"tests/acceptance/donna.md"
|
||||||
|
],
|
||||||
|
"assertions": [
|
||||||
|
"The final transcript QA passes.",
|
||||||
|
"The closing segment uses native tempo.",
|
||||||
|
"The response returns both audio and QA report paths."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"prompt": "Render this fast Producer and Writer exchange as a seamless Pitch Meeting dialogue, keeping speaker labels out of the audio.",
|
||||||
|
"expected_output": "A complete two-role MP3 with both roles, no spoken labels, and passing transcript QA.",
|
||||||
|
"files": [
|
||||||
|
"tests/acceptance/ryan-pitch-meeting.md"
|
||||||
|
],
|
||||||
|
"assertions": [
|
||||||
|
"Producer and Writer use their configured role references.",
|
||||||
|
"Speaker labels are not synthesized.",
|
||||||
|
"The final transcript QA passes."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"prompt": "Render this Val Holiday brief locally. Keep the greeting and sign-off native, normalize only substantive passages, and do not clip paragraph endings.",
|
||||||
|
"expected_output": "A complete Val Holiday MP3 with native short frames, 170 WPM long passages, natural tails, and passing QA.",
|
||||||
|
"files": [
|
||||||
|
"tests/acceptance/val-holiday.md"
|
||||||
|
],
|
||||||
|
"assertions": [
|
||||||
|
"Short greeting and closing remain at native tempo.",
|
||||||
|
"Long passages target 170 WPM.",
|
||||||
|
"Every target ending passes completeness checks."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"id": "chris-engineer",
|
||||||
|
"display_name": "Chris Engineer",
|
||||||
|
"adapter": "single",
|
||||||
|
"roles": {
|
||||||
|
"default": {
|
||||||
|
"reference_audio": "voices/chris-engineer/reference.wav",
|
||||||
|
"reference_transcript": "voices/chris-engineer/reference.txt"
|
||||||
|
},
|
||||||
|
"high-energy": {
|
||||||
|
"reference_audio": "voices/chris-engineer/reference-high-energy.wav",
|
||||||
|
"reference_transcript": "voices/chris-engineer/reference-high-energy.txt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deliveries": {
|
||||||
|
"narrative": {
|
||||||
|
"role": "default",
|
||||||
|
"tempo_multiplier": 1.12
|
||||||
|
},
|
||||||
|
"technical": {
|
||||||
|
"role": "default",
|
||||||
|
"tempo_multiplier": 1.12
|
||||||
|
},
|
||||||
|
"high-energy": {
|
||||||
|
"role": "high-energy",
|
||||||
|
"tempo_multiplier": 1.2
|
||||||
|
},
|
||||||
|
"joke": {
|
||||||
|
"role": "high-energy",
|
||||||
|
"tempo_multiplier": 1.2
|
||||||
|
},
|
||||||
|
"punchline": {
|
||||||
|
"role": "high-energy",
|
||||||
|
"tempo_multiplier": 1.2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"generation": {
|
||||||
|
"minimum_words": 4,
|
||||||
|
"maximum_words": 30,
|
||||||
|
"short_native_max_words": 10,
|
||||||
|
"default_context": "Wanna see something cool!!!"
|
||||||
|
},
|
||||||
|
"cadence": {
|
||||||
|
"default_mode": "multiplier",
|
||||||
|
"default_tempo_multiplier": 1.12,
|
||||||
|
"minimum_words_for_normalization": 10,
|
||||||
|
"minimum_tempo": 0.8,
|
||||||
|
"maximum_tempo": 1.25
|
||||||
|
},
|
||||||
|
"assembly": {
|
||||||
|
"crossfade_ms": 60,
|
||||||
|
"default_pause_ms": 120,
|
||||||
|
"minimum_natural_tail_ms": 200
|
||||||
|
},
|
||||||
|
"signature_assets": {
|
||||||
|
"opening": "voices/chris-engineer/signature-opening.wav"
|
||||||
|
},
|
||||||
|
"persona_guide": "voices/chris-engineer/persona.md"
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"id": "donna",
|
||||||
|
"display_name": "Donna",
|
||||||
|
"adapter": "single",
|
||||||
|
"roles": {
|
||||||
|
"default": {
|
||||||
|
"reference_audio": "voices/donna/reference-neutral.wav",
|
||||||
|
"reference_transcript": "voices/donna/reference-neutral.txt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"generation": {
|
||||||
|
"minimum_words": 4,
|
||||||
|
"maximum_words": 42,
|
||||||
|
"short_native_max_words": 18,
|
||||||
|
"default_context": ""
|
||||||
|
},
|
||||||
|
"cadence": {
|
||||||
|
"default_mode": "native",
|
||||||
|
"minimum_words_for_normalization": 20,
|
||||||
|
"minimum_tempo": 0.8,
|
||||||
|
"maximum_tempo": 1.25
|
||||||
|
},
|
||||||
|
"assembly": {
|
||||||
|
"crossfade_ms": 60,
|
||||||
|
"default_pause_ms": 100,
|
||||||
|
"minimum_natural_tail_ms": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"id": "grandpa-bomber",
|
||||||
|
"display_name": "Grandpa Bomber",
|
||||||
|
"adapter": "single",
|
||||||
|
"roles": {
|
||||||
|
"default": {
|
||||||
|
"reference_audio": "voices/grandpa-bomber/reference.wav",
|
||||||
|
"reference_transcript": "voices/grandpa-bomber/reference.txt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"generation": {
|
||||||
|
"minimum_words": 4,
|
||||||
|
"maximum_words": 45,
|
||||||
|
"short_native_max_words": 18,
|
||||||
|
"default_context": ""
|
||||||
|
},
|
||||||
|
"cadence": {
|
||||||
|
"default_mode": "native",
|
||||||
|
"minimum_words_for_normalization": 20,
|
||||||
|
"minimum_tempo": 0.8,
|
||||||
|
"maximum_tempo": 1.25
|
||||||
|
},
|
||||||
|
"assembly": {
|
||||||
|
"crossfade_ms": 60,
|
||||||
|
"default_pause_ms": 120,
|
||||||
|
"minimum_natural_tail_ms": 200
|
||||||
|
},
|
||||||
|
"persona_guide": "voices/grandpa-bomber/persona.md"
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"id": "ryan-pitch-meeting",
|
||||||
|
"display_name": "Ryan Pitch Meeting",
|
||||||
|
"adapter": "dialogue",
|
||||||
|
"roles": {
|
||||||
|
"producer": {
|
||||||
|
"reference_audio": "voices/ryan-pitch-meeting/producer-reference-v3.wav",
|
||||||
|
"reference_transcript": "voices/ryan-pitch-meeting/producer-reference-v3.txt",
|
||||||
|
"default_context": "I think that's like a slogan.",
|
||||||
|
"tempo_multiplier": 1.15
|
||||||
|
},
|
||||||
|
"writer": {
|
||||||
|
"reference_audio": "voices/ryan-pitch-meeting/writer-reference-v3.wav",
|
||||||
|
"reference_transcript": "voices/ryan-pitch-meeting/writer-reference-v3.txt",
|
||||||
|
"default_context": "No, no, no, that would never work.",
|
||||||
|
"tempo_multiplier": 1.3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"generation": {
|
||||||
|
"minimum_words": 4,
|
||||||
|
"maximum_words": 42,
|
||||||
|
"short_native_max_words": 6,
|
||||||
|
"default_context": ""
|
||||||
|
},
|
||||||
|
"cadence": {
|
||||||
|
"default_mode": "multiplier",
|
||||||
|
"minimum_words_for_normalization": 7,
|
||||||
|
"minimum_tempo": 0.8,
|
||||||
|
"maximum_tempo": 1.35
|
||||||
|
},
|
||||||
|
"assembly": {
|
||||||
|
"crossfade_ms": 35,
|
||||||
|
"default_pause_ms": 95,
|
||||||
|
"minimum_natural_tail_ms": 180
|
||||||
|
},
|
||||||
|
"signature_library": "voices/ryan-pitch-meeting/signature-exchanges/manifest.json"
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"id": "val-holiday",
|
||||||
|
"display_name": "Val Holiday",
|
||||||
|
"adapter": "single",
|
||||||
|
"roles": {
|
||||||
|
"default": {
|
||||||
|
"reference_audio": "voices/val-holiday/reference-v3.wav",
|
||||||
|
"reference_transcript": "voices/val-holiday/reference-v3.txt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deliveries": {
|
||||||
|
"narrative": {
|
||||||
|
"target_wpm": 170
|
||||||
|
},
|
||||||
|
"rollup": {
|
||||||
|
"target_wpm": 170
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"generation": {
|
||||||
|
"minimum_words": 4,
|
||||||
|
"maximum_words": 42,
|
||||||
|
"short_native_max_words": 18,
|
||||||
|
"default_context": ""
|
||||||
|
},
|
||||||
|
"cadence": {
|
||||||
|
"default_mode": "wpm",
|
||||||
|
"default_wpm": 170,
|
||||||
|
"minimum_words_for_normalization": 20,
|
||||||
|
"minimum_tempo": 0.75,
|
||||||
|
"maximum_tempo": 1.35
|
||||||
|
},
|
||||||
|
"assembly": {
|
||||||
|
"crossfade_ms": 60,
|
||||||
|
"default_pause_ms": 100,
|
||||||
|
"minimum_natural_tail_ms": 200
|
||||||
|
},
|
||||||
|
"pronunciation": {
|
||||||
|
"use_explicit_tts_text_for_homographs": true,
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"written": "lead",
|
||||||
|
"tts": "led",
|
||||||
|
"meaning": "bullets or ammunition"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"persona_guide": "voices/val-holiday/Val Holiday Pulse Brief Voice Guide.md"
|
||||||
|
}
|
||||||
Executable
+79
@@ -0,0 +1,79 @@
|
|||||||
|
#!/bin/zsh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: install_macos.sh --wheel PATH --assets PATH --model-dir PATH [--runtime PATH]"
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime_root="$HOME/Library/Application Support/MPM Local Voice/runtime"
|
||||||
|
wheel_path=""
|
||||||
|
assets_path=""
|
||||||
|
model_source=""
|
||||||
|
|
||||||
|
while (( $# )); do
|
||||||
|
case "$1" in
|
||||||
|
--runtime)
|
||||||
|
runtime_root="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--wheel)
|
||||||
|
wheel_path="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--assets)
|
||||||
|
assets_path="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--model-dir)
|
||||||
|
model_source="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$wheel_path" || -z "$assets_path" || -z "$model_source" ]]; then
|
||||||
|
usage
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
for required in "$wheel_path" "$assets_path" "$model_source"; do
|
||||||
|
if [[ ! -e "$required" ]]; then
|
||||||
|
echo "Missing required input: $required" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! command -v python3.11 >/dev/null 2>&1; then
|
||||||
|
echo "Python 3.11 is required. Install it with Homebrew first." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if ! command -v ffmpeg >/dev/null 2>&1; then
|
||||||
|
echo "ffmpeg is required and must be available on PATH." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if ! command -v whisper >/dev/null 2>&1; then
|
||||||
|
echo "Whisper is required and must be available on PATH." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$runtime_root/models" "$runtime_root/jobs" "$runtime_root/wheels"
|
||||||
|
|
||||||
|
python3.11 -m venv "$runtime_root/.venv"
|
||||||
|
"$runtime_root/.venv/bin/python" -m pip install --upgrade pip
|
||||||
|
cp -p "$wheel_path" "$runtime_root/wheels/"
|
||||||
|
"$runtime_root/.venv/bin/python" -m pip install "$wheel_path"
|
||||||
|
"$runtime_root/.venv/bin/python" -m pip install numpy==2.4.6 soundfile==0.14.0
|
||||||
|
|
||||||
|
tar -xzf "$assets_path" -C "$runtime_root"
|
||||||
|
|
||||||
|
model_target="$runtime_root/models/CosyVoice3-0.5B-Candle"
|
||||||
|
mkdir -p "$model_target"
|
||||||
|
rsync -a "$model_source/" "$model_target/"
|
||||||
|
|
||||||
|
echo "Runtime installed at: $runtime_root"
|
||||||
|
echo "Run scripts/verify_install.py with:"
|
||||||
|
echo "\"$runtime_root/.venv/bin/python\" scripts/verify_install.py --runtime-root \"$runtime_root\" --device metal"
|
||||||
Executable
+1320
File diff suppressed because it is too large
Load Diff
Executable
+44
@@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run a deterministic Local Voice installation preflight."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
RENDERER = ROOT / "scripts" / "local_voice.py"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--runtime-root")
|
||||||
|
parser.add_argument("--device", choices=("metal", "cpu", "cuda"), default="metal")
|
||||||
|
args = parser.parse_args()
|
||||||
|
command = [
|
||||||
|
sys.executable,
|
||||||
|
str(RENDERER),
|
||||||
|
"doctor",
|
||||||
|
"--device",
|
||||||
|
args.device,
|
||||||
|
]
|
||||||
|
if args.runtime_root:
|
||||||
|
command.extend(["--runtime-root", args.runtime_root])
|
||||||
|
completed = subprocess.run(command, check=False)
|
||||||
|
if completed.returncode:
|
||||||
|
return completed.returncode
|
||||||
|
completed = subprocess.run(
|
||||||
|
[sys.executable, str(RENDERER), "list"],
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
if completed.returncode:
|
||||||
|
return completed.returncode
|
||||||
|
print("Local Voice installation preflight passed.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
---
|
||||||
|
name: local-voice
|
||||||
|
description: >
|
||||||
|
Generate, validate, resume, and assemble local text-to-speech audio using the
|
||||||
|
approved Donna, Chris Engineer, Grandpa Bomber, Ryan Pitch Meeting, and Val
|
||||||
|
Holiday profiles. Use this skill whenever a user asks to make a voice brief,
|
||||||
|
local TTS message, spoken notification, character dialogue, CosyVoice render,
|
||||||
|
MP3 briefing, or asks another skill such as Donna to produce audio locally.
|
||||||
|
Also use it to list voices, diagnose the local voice stack, prepare a render
|
||||||
|
plan, or correct pronunciation and audio seams.
|
||||||
|
metadata:
|
||||||
|
version: "0.1.0"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Local Voice
|
||||||
|
|
||||||
|
Use the shared renderer as an audio dependency. Keep briefing intelligence and
|
||||||
|
persona writing in the calling skill; keep chunking, pronunciation, generation,
|
||||||
|
alignment, timing, and assembly here.
|
||||||
|
|
||||||
|
The skill requires the separately installed Local Voice runtime, CosyVoice3
|
||||||
|
Candle model, ffmpeg, Whisper, and authorized reference audio. Apple Silicon
|
||||||
|
Metal is the verified production path.
|
||||||
|
|
||||||
|
## Before rendering
|
||||||
|
|
||||||
|
1. Read `references/voice-catalog.md` for the selected voice.
|
||||||
|
2. Read `references/render-plan-schema.md`.
|
||||||
|
3. Read the selected voice's persona guide from the configured runtime when the
|
||||||
|
request includes script writing, not just rendering.
|
||||||
|
4. Preserve two forms of every passage:
|
||||||
|
- `text`: canonical readable wording.
|
||||||
|
- `tts_text`: optional pronunciation-safe wording sent only to the engine.
|
||||||
|
5. Never include Markdown headings, speaker labels, stage directions, or
|
||||||
|
renderer metadata in spoken text.
|
||||||
|
|
||||||
|
## Core production rules
|
||||||
|
|
||||||
|
- Generate the largest safe group of complete sentences or paragraphs.
|
||||||
|
- Treat paragraph boundaries as candidate splits, not mandatory audio cuts.
|
||||||
|
- Preserve short openings, closings, reactions, and signature clips at native
|
||||||
|
tempo. WPM normalization is unreliable on short clips.
|
||||||
|
- Apply speed adjustment only to sufficiently long passages and only within the
|
||||||
|
profile's validated correction range.
|
||||||
|
- Create drawl, emphasis, and emotional weight through punctuation and wording,
|
||||||
|
not global slowdown.
|
||||||
|
- Use actual neighboring script text as sacrificial context when a cold start
|
||||||
|
needs trimming. Do not use unrelated generic carriers.
|
||||||
|
- Retain natural word-release tails and join at quiet waveform boundaries.
|
||||||
|
- Keep raw WAV and alignment files so `--assemble-only` can repair a brief
|
||||||
|
without regenerating model output.
|
||||||
|
- Validate transcript completeness before changing cadence or assembling.
|
||||||
|
- Prefer an approved fixed asset for Ryan's very short reactions and exact
|
||||||
|
signature exchanges.
|
||||||
|
- Never publish or redistribute reference audio unless the user has rights to
|
||||||
|
that asset.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### 1. Check the runtime
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/local_voice.py" doctor
|
||||||
|
```
|
||||||
|
|
||||||
|
In Codex, resolve the plugin root from this skill's location if
|
||||||
|
`${CLAUDE_PLUGIN_ROOT}` is unavailable.
|
||||||
|
|
||||||
|
### 2. Build or validate a render plan
|
||||||
|
|
||||||
|
For simple single-voice prose:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/local_voice.py" plan \
|
||||||
|
--voice val-holiday \
|
||||||
|
--script /absolute/path/brief.md \
|
||||||
|
--output-plan /absolute/path/render-plan.json \
|
||||||
|
--output-audio /absolute/path/brief.mp3
|
||||||
|
```
|
||||||
|
|
||||||
|
For Chris, Ryan, signature assets, pronunciation overrides, or deliberate
|
||||||
|
delivery changes, write the JSON plan directly and validate it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/local_voice.py" validate \
|
||||||
|
/absolute/path/render-plan.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Render
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/local_voice.py" render \
|
||||||
|
/absolute/path/render-plan.json \
|
||||||
|
--resume
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `--assemble-only` when all required raw WAV files already exist.
|
||||||
|
|
||||||
|
### 4. Check the report
|
||||||
|
|
||||||
|
Do not report success until the command produces:
|
||||||
|
|
||||||
|
- the requested WAV or MP3;
|
||||||
|
- a QA JSON file;
|
||||||
|
- complete target text for every generated segment;
|
||||||
|
- no rejected cadence correction;
|
||||||
|
- no missing fixed or suffix asset.
|
||||||
|
|
||||||
|
If a segment truncates, split it at a complete sentence boundary and regenerate
|
||||||
|
only that segment. If a seam clicks, retain more tail or move the cut to a quiet
|
||||||
|
boundary; do not keep regenerating speech that already passed.
|
||||||
|
|
||||||
|
## Dependency contract
|
||||||
|
|
||||||
|
Calling skills provide:
|
||||||
|
|
||||||
|
- selected `voice`;
|
||||||
|
- canonical script or structured dialogue;
|
||||||
|
- desired output path;
|
||||||
|
- optional role, delivery class, fixed asset, pronunciation override, and
|
||||||
|
pause metadata.
|
||||||
|
|
||||||
|
Local Voice returns:
|
||||||
|
|
||||||
|
- final audio path;
|
||||||
|
- QA report path;
|
||||||
|
- duration;
|
||||||
|
- segment-level completeness and cadence results;
|
||||||
|
- a clear failure with the segment ID when human review or regeneration is
|
||||||
|
required.
|
||||||
|
|
||||||
|
Do not silently fall back to cloud TTS. Let the calling skill decide whether to
|
||||||
|
use ElevenLabs, text-only delivery, or another fallback.
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# Render plan schema
|
||||||
|
|
||||||
|
Use JSON with this structure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"job_id": "morning-brief-2026-07-27",
|
||||||
|
"voice": "val-holiday",
|
||||||
|
"output": "/absolute/path/brief.mp3",
|
||||||
|
"segments": [
|
||||||
|
{
|
||||||
|
"id": "01-greeting",
|
||||||
|
"text": "Morning, Tracy. Let us get straight to the point.",
|
||||||
|
"native_tempo": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "02-update",
|
||||||
|
"text": "The team closed the issue and documented the root cause.",
|
||||||
|
"tts_text": "The team closed the issue and documented the root cause.",
|
||||||
|
"delivery": "narrative",
|
||||||
|
"context_before": "Let us get straight to the point.",
|
||||||
|
"pause_after_ms": 100
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plan fields
|
||||||
|
|
||||||
|
### Root
|
||||||
|
|
||||||
|
| Field | Required | Meaning |
|
||||||
|
|---|---:|---|
|
||||||
|
| `schema_version` | Yes | Currently `1.0`. |
|
||||||
|
| `job_id` | Yes | Stable filesystem-safe identifier. |
|
||||||
|
| `voice` | Yes | Profile ID from the voice catalog. |
|
||||||
|
| `output` | Yes | Absolute `.wav` or `.mp3` destination. |
|
||||||
|
| `segments` | Yes | Ordered list of speech or fixed assets. |
|
||||||
|
| `final_transcript_qa` | No | Defaults to `true`. |
|
||||||
|
|
||||||
|
### Segment
|
||||||
|
|
||||||
|
| Field | Required | Meaning |
|
||||||
|
|---|---:|---|
|
||||||
|
| `id` | Yes | Unique filesystem-safe ID. |
|
||||||
|
| `text` | Yes | Canonical readable transcript. |
|
||||||
|
| `tts_text` | No | Pronunciation-safe engine text. |
|
||||||
|
| `role` | No | Role within a multi-role voice, such as `producer`. |
|
||||||
|
| `delivery` | No | Profile delivery class, such as `high-energy`. |
|
||||||
|
| `native_tempo` | No | Disable all tempo processing for this segment. |
|
||||||
|
| `target_wpm` | No | Override the profile WPM for a long passage. |
|
||||||
|
| `tempo_multiplier` | No | Explicit validated multiplier for a complete passage. |
|
||||||
|
| `context_before` | No | Sacrificial neighboring text trimmed before target. |
|
||||||
|
| `context_after` | No | Sacrificial neighboring text trimmed after target. |
|
||||||
|
| `fixed_asset` | No | Authorized asset path relative to runtime root. |
|
||||||
|
| `asset_pool` | No | Authorized asset paths; one is selected deterministically. |
|
||||||
|
| `suffix_asset` | No | Asset appended to the generated passage. |
|
||||||
|
| `suffix_gap_ms` | No | Silence before suffix asset. |
|
||||||
|
| `pause_after_ms` | No | Silence after the assembled segment. |
|
||||||
|
|
||||||
|
Do not combine `fixed_asset` with generated `tts_text`. A fixed segment still
|
||||||
|
needs canonical `text` for the final transcript and report.
|
||||||
|
|
||||||
|
## Pronunciation examples
|
||||||
|
|
||||||
|
Keep canonical text:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"text": "Nobody else wastes lead on it."
|
||||||
|
```
|
||||||
|
|
||||||
|
Send the engine:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"tts_text": "Nobody else wastes led on it."
|
||||||
|
```
|
||||||
|
|
||||||
|
Use explicit overrides for homographs. Do not globally replace every instance of
|
||||||
|
`lead`, because leadership and ammunition require different pronunciations.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Voice catalog
|
||||||
|
|
||||||
|
## Donna
|
||||||
|
|
||||||
|
- ID: `donna`
|
||||||
|
- Type: single voice
|
||||||
|
- Reference: neutral designed-voice recording
|
||||||
|
- Default: native timing, punctuation-led expression
|
||||||
|
- Use for Bryan's primary Donna brief and lower-priority notifications
|
||||||
|
- Do not feed natural-language shaping instructions into zero-shot production
|
||||||
|
|
||||||
|
## Chris Engineer
|
||||||
|
|
||||||
|
- ID: `chris-engineer`
|
||||||
|
- Type: single voice with narrative and high-energy references
|
||||||
|
- Use the authentic fixed opening for `Wanna see something cool!!!`
|
||||||
|
- Narrative and technical explanation use the narrative reference
|
||||||
|
- Jokes, punchlines, sharp warnings, and the closing tag use high energy
|
||||||
|
- Change delivery only at a complete sentence boundary
|
||||||
|
|
||||||
|
## Grandpa Bomber
|
||||||
|
|
||||||
|
- ID: `grandpa-bomber`
|
||||||
|
- Type: single voice
|
||||||
|
- Default: native timing
|
||||||
|
- Shape sleepy setup and deadpan escalation through punctuation
|
||||||
|
- Keep the edge PG and use `freaking`, not `effing` or `fudge`
|
||||||
|
|
||||||
|
## Ryan Pitch Meeting
|
||||||
|
|
||||||
|
- ID: `ryan-pitch-meeting`
|
||||||
|
- Type: two roles from the same performer
|
||||||
|
- Roles: `producer`, `writer`
|
||||||
|
- Writer is faster and more eager; Producer is calmer and skeptical
|
||||||
|
- Use fixed assets for exact short signatures and reactions when available
|
||||||
|
- Preserve role labels in the plan but never synthesize them
|
||||||
|
|
||||||
|
## Val Holiday
|
||||||
|
|
||||||
|
- ID: `val-holiday`
|
||||||
|
- Type: single voice
|
||||||
|
- Long substantive passages target 170 WPM
|
||||||
|
- Short greeting, closing, and reaction passages stay at native timing
|
||||||
|
- Do not create drawl by slowing playback
|
||||||
|
- Use punctuation and sentence construction for weighted delivery
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Three warning lights all trace back to one loose connector. That is not three failures; it is one design flaw wearing three different hats.
|
||||||
|
|
||||||
|
Replace the connector, document the root cause, and stop blaming the operator for what the hardware did.
|
||||||
|
|
||||||
|
Fix the system once... and that is pretty cool.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Good morning. Overnight, the service team closed the kiosk alert and confirmed the replacement controller is stable.
|
||||||
|
|
||||||
|
One decision remains: approve the revised field schedule before noon so the Monday installation stays on track.
|
||||||
|
|
||||||
|
That is the whole story. Handle the schedule, then enjoy the quiet.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Morning. We have one little weather problem parked on the runway.
|
||||||
|
|
||||||
|
The supplier missed the shipment, the field crew is waiting, and somebody scheduled the backup truck for the wrong county. Find the truck before I have to drop the sun on the dispatch board.
|
||||||
|
|
||||||
|
That is all. Now get out of my briefing room before something starts leaking hydraulic fluid.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
**PRODUCER GUY:** So, you have a Monday briefing for me?
|
||||||
|
|
||||||
|
**WRITER GUY:** Yes sir, I do! The Phoenix hardware shipped, but the final quantity approval is still missing!
|
||||||
|
|
||||||
|
**PRODUCER GUY:** Oh, paperwork blocking shipped hardware is tight!
|
||||||
|
|
||||||
|
**PRODUCER GUY:** Is it going to be hard to clear that before noon?
|
||||||
|
|
||||||
|
**WRITER GUY:** Actually, it's gonna be super easy, barely an inconvenience! Oh, really?
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Morning, Tracy.
|
||||||
|
|
||||||
|
The Phoenix installation is ready, but the final quantity approval remains unsigned. The hardware is already moving, so paperwork is the only gentleman still blocking the road.
|
||||||
|
|
||||||
|
The engineering team settled yesterday's controller affair and documented the root cause. A clean ending, for once.
|
||||||
|
|
||||||
|
Sign the quantity approval before noon. Say when.
|
||||||
Executable
+181
@@ -0,0 +1,181 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Create and optionally render fresh Local Voice acceptance plans."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
RENDERER = ROOT / "scripts" / "local_voice.py"
|
||||||
|
CASES = (
|
||||||
|
"donna",
|
||||||
|
"chris-engineer",
|
||||||
|
"grandpa-bomber",
|
||||||
|
"val-holiday",
|
||||||
|
"ryan-pitch-meeting",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run(command: list[str]) -> None:
|
||||||
|
subprocess.run(command, check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--runtime-root", required=True, type=Path)
|
||||||
|
parser.add_argument("--output-dir", required=True, type=Path)
|
||||||
|
parser.add_argument("--device", choices=("metal", "cpu", "cuda"), default="metal")
|
||||||
|
parser.add_argument("--render", action="store_true")
|
||||||
|
parser.add_argument(
|
||||||
|
"--voices",
|
||||||
|
help="Comma-separated subset of profile IDs; defaults to every case.",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
selected_cases = (
|
||||||
|
tuple(item.strip() for item in args.voices.split(",") if item.strip())
|
||||||
|
if args.voices
|
||||||
|
else CASES
|
||||||
|
)
|
||||||
|
unknown = sorted(set(selected_cases) - set(CASES))
|
||||||
|
if unknown:
|
||||||
|
parser.error("Unknown acceptance voice(s): " + ", ".join(unknown))
|
||||||
|
results = []
|
||||||
|
for voice in selected_cases:
|
||||||
|
script = ROOT / "tests" / "acceptance" / f"{voice}.md"
|
||||||
|
plan = args.output_dir / f"{voice}.json"
|
||||||
|
audio = args.output_dir / f"{voice}.mp3"
|
||||||
|
job_id = f"acceptance-v9-{voice}"
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
str(RENDERER),
|
||||||
|
"plan",
|
||||||
|
"--voice",
|
||||||
|
voice,
|
||||||
|
"--script",
|
||||||
|
str(script),
|
||||||
|
"--output-plan",
|
||||||
|
str(plan),
|
||||||
|
"--output-audio",
|
||||||
|
str(audio),
|
||||||
|
"--job-id",
|
||||||
|
job_id,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if voice == "ryan-pitch-meeting":
|
||||||
|
data = json.loads(plan.read_text(encoding="utf-8"))
|
||||||
|
data["segments"] = [
|
||||||
|
{
|
||||||
|
"id": "01-producer",
|
||||||
|
"role": "producer",
|
||||||
|
"text": "So, you have a Monday briefing for me?",
|
||||||
|
"context_after": "Yes sir, I do!",
|
||||||
|
"pause_after_ms": 95,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "02-yes-sir",
|
||||||
|
"role": "writer",
|
||||||
|
"text": "Yes sir, I do!",
|
||||||
|
"fixed_asset": (
|
||||||
|
"voices/ryan-pitch-meeting/signature-exchanges/"
|
||||||
|
"yes-sir-i-do/lotr_two_towers.wav"
|
||||||
|
),
|
||||||
|
"pause_after_ms": 55,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "03-writer",
|
||||||
|
"role": "writer",
|
||||||
|
"text": (
|
||||||
|
"The Phoenix hardware shipped, but the final quantity "
|
||||||
|
"approval is still missing!"
|
||||||
|
),
|
||||||
|
"context_before": "Yes sir, I do!",
|
||||||
|
"context_after": (
|
||||||
|
"Oh, paperwork blocking shipped hardware is tight!"
|
||||||
|
),
|
||||||
|
"pause_after_ms": 95,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "04-tight",
|
||||||
|
"role": "producer",
|
||||||
|
"text": "Oh, paperwork blocking shipped hardware is tight!",
|
||||||
|
"tts_text": "Oh, paperwork blocking shipped hardware",
|
||||||
|
"context_before": (
|
||||||
|
"The final quantity approval is still missing!"
|
||||||
|
),
|
||||||
|
"context_after": "It sure is!",
|
||||||
|
"suffix_asset": (
|
||||||
|
"voices/ryan-pitch-meeting/signature-exchanges/"
|
||||||
|
"tight-endings/is/disclosure.wav"
|
||||||
|
),
|
||||||
|
"suffix_gap_ms": 130,
|
||||||
|
"pause_after_ms": 95,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "05-producer",
|
||||||
|
"role": "producer",
|
||||||
|
"text": (
|
||||||
|
"Is it going to be hard to clear that before noon?"
|
||||||
|
),
|
||||||
|
"context_before": (
|
||||||
|
"Oh, paperwork blocking shipped hardware is tight!"
|
||||||
|
),
|
||||||
|
"context_after": (
|
||||||
|
"Actually, it's gonna be super easy, barely an "
|
||||||
|
"inconvenience! Oh, really?"
|
||||||
|
),
|
||||||
|
"pause_after_ms": 95,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "06-super-easy",
|
||||||
|
"role": "writer",
|
||||||
|
"text": (
|
||||||
|
"Actually, it's gonna be super easy, barely an "
|
||||||
|
"inconvenience! Oh, really?"
|
||||||
|
),
|
||||||
|
"fixed_asset": (
|
||||||
|
"voices/ryan-pitch-meeting/signature-exchanges/"
|
||||||
|
"super_easy_oh_really.wav"
|
||||||
|
),
|
||||||
|
"pause_after_ms": 95,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
plan.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
||||||
|
run([sys.executable, str(RENDERER), "validate", str(plan)])
|
||||||
|
if args.render:
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
str(RENDERER),
|
||||||
|
"render",
|
||||||
|
str(plan),
|
||||||
|
"--runtime-root",
|
||||||
|
str(args.runtime_root),
|
||||||
|
"--device",
|
||||||
|
args.device,
|
||||||
|
"--resume",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
results.append({"voice": voice, "plan": str(plan), "audio": str(audio)})
|
||||||
|
summary = args.output_dir / "acceptance-summary.json"
|
||||||
|
existing = []
|
||||||
|
if summary.exists():
|
||||||
|
existing = json.loads(summary.read_text(encoding="utf-8"))
|
||||||
|
merged = {item["voice"]: item for item in existing}
|
||||||
|
merged.update({item["voice"]: item for item in results})
|
||||||
|
summary.write_text(
|
||||||
|
json.dumps([merged[key] for key in sorted(merged)], indent=2) + "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
print(f"Acceptance summary: {summary}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user