feat: add CW-026 Local Voice plugin

This commit is contained in:
Bryan Gilliom
2026-07-27 00:33:25 +08:00
commit 5d680928b9
29 changed files with 2697 additions and 0 deletions
+17
View File
@@ -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"
]
}
+26
View File
@@ -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
View File
@@ -0,0 +1,17 @@
.DS_Store
__pycache__/
*.pyc
.venv/
runtime/
generated/
outputs/
models/
voice-assets/
*.wav
*.mp3
*.m4a
*.flac
*.onnx
*.safetensors
*.plugin
*.zip
+12
View File
@@ -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.
+17
View File
@@ -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.
+120
View File
@@ -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.
+46
View File
@@ -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` |
+62
View File
@@ -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 2042
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.
+132
View File
@@ -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.
+31
View File
@@ -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.
+52
View File
@@ -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
+44
View File
@@ -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."
]
}
]
}
+60
View File
@@ -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"
}
+29
View File
@@ -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
}
}
+30
View File
@@ -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"
}
+38
View File
@@ -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"
}
+49
View File
@@ -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"
}
+79
View File
@@ -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"
+1320
View File
File diff suppressed because it is too large Load Diff
+44
View File
@@ -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())
+135
View File
@@ -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
+5
View File
@@ -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.
+5
View File
@@ -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.
+5
View File
@@ -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.
+9
View File
@@ -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?
+7
View File
@@ -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.
+181
View File
@@ -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())