Skip to content

feat: per-agent voice pilot — VoiceAdapter + OpenClaw demo (3 presets) - #8

Merged
lukejmorrison merged 41 commits into
mainfrom
feature/voicedna-openclaw-per-agent-voices
Apr 20, 2026
Merged

feat: per-agent voice pilot — VoiceAdapter + OpenClaw demo (3 presets)#8
lukejmorrison merged 41 commits into
mainfrom
feature/voicedna-openclaw-per-agent-voices

Conversation

@lukejmorrison

@lukejmorrison lukejmorrison commented Apr 18, 2026

Copy link
Copy Markdown
Owner

Feature: Per-Agent Voice Presets for OpenClaw

Summary

Wire the VoiceDNA VoiceAdapter into the OpenClaw agent voice pipeline, enabling agents to use distinct voice presets (neutral, friendly, flair) based on agent identity. Fully opt-in via env vars — default VoiceDNA behavior is unchanged.

Branch

feature/voicedna-openclaw-per-agent-voices

Changes

New Modules

File Purpose
voicedna/openclaw_adapter.py VoiceAdapter class: per-agent preset selection + synthesis
voicedna/openclaw_live_voice.py render_agent_voice() — OpenClaw TTS hook entry point
examples/openclaw_voicedemo.py Demo: 3 agents × 3 presets → WAV output
tests/test_voice_adapter.py Unit/smoke tests for VoiceAdapter
tests/test_openclaw_live_voice.py Integration tests: demo, agent mapping, live bridge

Key Features

  1. Preset Registry — Three pilot presets (neutral, friendly, flair) with voice DNA parameters
  2. Agent Mapping — Environment-driven (JSON) or programmatic agent_id → preset resolution
  3. Fallback Chainagent_idagent_namedefault_preset
  4. Opt-in ActivationVOICEDNA_OPENCLAW_PRESETS=1 activates the hook; absent = no-op
  5. No Breaking Changes — Existing VoiceDNA CLI/SDK behavior is unchanged

Test Instructions

cd /path/to/VoiceDNA

# Unit + integration tests
python -m pytest tests/test_voice_adapter.py tests/test_openclaw_live_voice.py -q

# Full suite
python -m pytest -q

# Demo smoke test (produces 3 WAV files)
python -m examples.openclaw_voicedemo

Expected Outputs

File Description
examples/openclaw/output/namshub_neutral.wav Namshub — neutral preset
examples/openclaw/output/david_friendly.wav David Hardman — friendly preset
examples/openclaw/output/voss_flair.wav Dr Voss Thorne — flair preset

All three files should be non-empty RIFF/WAVE format (≥ 100 KB each at 22050 Hz 16-bit mono).

Configuration

# Production opt-in
export VOICEDNA_OPENCLAW_PRESETS=1
export VOICEDNA_OPENCLAW_PRESETS_MAP='{"agent:namshub":"neutral","agent:david-hardman":"friendly","agent:dr-voss-thorne":"flair"}'

Programmatic Usage

from voicedna.openclaw_live_voice import render_agent_voice

wav_bytes = render_agent_voice(
    text=tts_text,
    agent_id=agent.id,
    agent_name=getattr(agent, "name", None),
    output_path=maybe_output_path,
)
# returns None when VOICEDNA_OPENCLAW_PRESETS is not set (no-op)

Or use the adapter directly:

from voicedna.openclaw_adapter import VoiceAdapter

adapter = VoiceAdapter(agent_presets={
    "agent:namshub": "neutral",
    "agent:dr-voss-thorne": "flair",
})
preset = adapter.select_preset("agent:namshub")
wav = adapter.synthesize("Hello.", preset, output_path="output.wav")

Rollback

  1. Unset VOICEDNA_OPENCLAW_PRESETS and VOICEDNA_OPENCLAW_PRESETS_MAP
  2. Remove the render_agent_voice() import from the OpenClaw hook
  3. Return to the previous TTS path

No schema migration or persistent state required.

Validation Results

  • ✅ 52 tests pass (31 unit + 21 integration)
  • ✅ Demo produces 3 valid WAV files (164–212 KB each)
  • ✅ WAVs are valid RIFF PCM 16-bit mono 22050 Hz
  • VOICEDNA_OPENCLAW_PRESETS guard confirmed: returns None when unset
  • ✅ Linting passes (ruff)
  • ✅ No changes to existing VoiceDNA public API

Dr Voss Thorne and others added 30 commits April 13, 2026 09:01
- voicedna/openclaw_adapter.py: new opt-in per-agent voice routing module
- Pilot presets: neutral, friendly, flair
- Env-based mapping via VOICEDNA_OPENCLAW_PRESETS_MAP
- Falls back to DEFAULT_PRESET for unmapped agents
- No changes to existing CLI or public SDK
- examples/openclaw_voicedemo.py: runnable local demo
- examples/openclaw/output/: namshub_neutral, david_friendly, voss_flair WAVs
- No cloud deps; espeak-ng or synthetic tone fallback
…ATION_NOTE

- README: Per-agent voices for OpenClaw pilot — usage, presets table, API example
- CHANGELOG: unreleased entry for feature branch
- IMPLEMENTATION_NOTE.md: run/test instructions and assumptions
…ot ship

- Re-ran local demo (piper/local TTS backend, no licensed audio)
- All 34 pytest tests pass, ruff clean
- Added QA_checklist, pr_description_summary, release_notes to pr_prep/
…port.txt

- Applied ruff format to 31 files (no logic changes, whitespace/style only)
- Regenerated 3 demo WAVs via openclaw_voicedemo.py (all pass)
- Updated local_test_report.txt: 34 tests pass, ruff clean, demo clean
- Branch ready for merge
…ent-voices' into feature/voicedna-openclaw-per-agent-voices
- New test_openclaw_live_voice.py: 13 integration tests validating demo, agent mapping, preset registry, and e2e synthesis
- VERIFICATION_REPORT.md: comprehensive verification showing 31/31 tests pass, linting passes, demo produces valid WAVs
- PR_OPENCLAW_VOICES.md: concise PR description for feature review

All success criteria met:
✓ 31 tests pass (18 unit + 13 integration)
✓ Demo produces 3 valid RIFF/WAVE files (164-211 KB)
✓ End-to-end synthesis validated
✓ Agent ID format handling works
✓ No breaking changes
Comprehensive handoff showing all success criteria met, test results,
artifact validation, and push instructions for review team.
Complete record of:
- 31/31 tests passing (18 unit + 13 integration)
- Linting passes (ruff all-clear)
- 3 valid WAV artifacts with file format validation
- Success criteria checklist
- Branch ready for push
Final comprehensive summary showing:
- All success criteria met
- 31/31 tests pass
- Demo validates
- Branch ready for push
- Full audit trail and next steps
…sh PR_BODY

- voicedna/openclaw_live_voice.py: render_agent_voice() opt-in guard + lazy
  VoiceAdapter cache; reset_adapter() for test isolation
- tests/test_openclaw_live_voice.py: +5 TestRenderAgentVoice tests covering
  opt-in guard (None when env unset), synthesis path, alias resolution,
  and adapter cache reset
- PR_BODY.md: updated with final test counts (52 passed), openclaw_live_voice
  usage example, and validation results
- examples/openclaw/output/: refreshed demo WAVs (164-212 KB each)

All 52 tests pass locally.
@lukejmorrison
lukejmorrison marked this pull request as ready for review April 19, 2026 14:22
Copilot AI review requested due to automatic review settings April 19, 2026 14:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in OpenClaw integration layer to route agent speech through VoiceDNA “pilot” presets (neutral/friendly/flair), including a live hook entrypoint, demo script, and tests; plus repo-wide formatting/cleanup and extensive supporting docs/artifacts.

Changes:

  • Introduce OpenClaw-facing live hook (render_agent_voice) with env-gated enablement and preset mapping.
  • Add/expand tests + demo to validate per-agent preset selection and WAV outputs.
  • Refactor/format multiple modules and adjust package import behavior to keep optional heavy deps from breaking lightweight usage.

Reviewed changes

Copilot reviewed 112 out of 121 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
vst3/venom_bridge.py Formatting changes; no functional additions apparent in diff.
vst3/bridge_runtime.py Formatting changes; no functional additions apparent in diff.
voicedna/synthesis.py Formatting changes in backend selection / subprocess calls.
voicedna/providers/piper.py Formatting + small refactor of env parsing and command construction.
voicedna/providers/personaplex.py Formatting + minor refactors; error strings adjusted.
voicedna/plugins/manager.py Formatting changes for readability.
voicedna/openclaw_live_voice.py New OpenClaw live bridge entrypoint with env-gated routing + adapter caching.
voicedna/framework.py Formatting changes + clearer error raising.
voicedna/filters/imprint_converter.py Formatting changes + string literal quoting updates.
voicedna/filters/audio_helpers.py Formatting changes.
voicedna/filters/age_maturation.py Formatting changes.
voicedna/consistency.py Remove unused import + formatting changes.
voicedna/__init__.py Make top-level imports optional via try/except ModuleNotFoundError.
voice_dna.py Formatting changes + minor readability refactors.
tests/test_voice_adapter.py New unit/smoke tests for VoiceAdapter preset routing and synthesis behavior.
tests/test_openclaw_live_voice.py New integration tests for demo artifacts + live hook behavior.
tests/test_processor_report.py Formatting changes.
tests/test_natural_doctor.py Formatting changes.
tests/test_consistency_engine.py Formatting changes.
tests/test_audio_roundtrip.py Formatting changes.
tests/conftest.py Formatting-only impact in shown diff.
conftest.py New root conftest adding repo root to sys.path for pytest collection.
examples/openclaw_voicedemo.py New demo producing 3 WAVs for 3 agents/presets.
examples/voicebox_demo.py Formatting changes.
examples/openclaw_skill.py Formatting changes.
examples/openclaw_hook.py Formatting changes.
examples/openclaw/voipms_phone_skill.py Formatting changes + line wrapping.
examples/openclaw/voicedna_tts_hook.py Formatting changes.
examples/omarchy/voicedna-pipewire-filter.py Formatting changes + small readability improvements.
examples/omarchy/voicedna-os-daemon.py Formatting changes + line wrapping.
examples/encrypted_plugin_demo.py Formatting changes.
examples/elevenlabs_demo.py Formatting changes.
examples/create_from_audio.py Formatting changes.
examples/cartesia_demo.py Formatting changes.
README.md Document OpenClaw per-agent pilot usage and presets.
CHANGELOG.md Add unreleased entry describing OpenClaw per-agent pilot changes.
scripts/review_feedback.py Formatting changes.
test_results.log Added test output artifact.
test_logs.txt Added error-log artifact.
test-output.txt Added test output artifact.
ruff_results.log Added lint output artifact.
LINT_LOG.txt Added lint/mypy notes artifact.
TEST_LOG.txt Added test output artifact.
TEST_LOGS.txt Added summarized test output artifact.
local_test_report.txt Added local verification report artifact.
missing_items.txt Added checklist/notes artifact.
approval_request.txt Added approval/push notes artifact.
PR_BODY.md Added PR body content artifact.
PR_CHECKLIST.md Added PR checklist artifact.
PR_DESCRIPTION.md Added PR description artifact.
PR_DRAFT.md Added PR draft artifact.
PR_OPENCLAW_VOICES.md Added PR narrative artifact.
PR_READY_REPORT.md Added PR readiness artifact.
PR_ready.md Added PR prep TODO artifact.
PR_description.txt Added PR text artifact.
README_DELIVERABLES.md Added deliverables index artifact.
HANDOFF.md Added handoff doc artifact.
IMPLEMENTATION_NOTE.md Added implementation notes artifact.
INTEGRATION_NOTE.md Added integration notes artifact.
DESIGN_DOC.md Added/updated design doc artifact.
research/* Added extensive research/verification/design/push documentation artifacts.
release/* Added release/push/verification artifacts and helper scripts.
release_prep/* Added release prep instructions/artifacts.
push-plan.md / push-checklist.md Added push planning artifacts.
pr_prep/* Added PR prep artifacts/checklists.
.task.md Added task brief artifact.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +241 to +245
dna = _build_dna_for_preset(preset)
raw_audio = self._tts.synthesize(text)

preset_cfg = PRESET_REGISTRY[preset]
process_params: Dict[str, Any] = {

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VoiceAdapter.synthesize() calls self._tts.synthesize(text) to compute raw_audio, but later calls VoiceDNAProcessor.synthesize_and_process(..., tts_provider=self._tts) which will call tts_provider.synthesize(text) again. This results in synthesizing the same text twice per request (extra latency + nondeterministic output if backend varies). Consider either (a) calling processor.process(raw_audio, dna, process_params) directly, or (b) removing the raw_audio call and relying solely on synthesize_and_process (keeping the AttributeError fallback consistent).

Copilot uses AI. Check for mistakes.
Comment on lines +252 to +261
try:
processed = self._processor.synthesize_and_process(
text=text,
dna=dna,
tts_provider=self._tts,
params=process_params,
)
except AttributeError:
# Older VoiceDNAProcessor: use process() directly
processed = self._processor.process(raw_audio, dna, process_params)

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if you keep synthesize_and_process(), the AttributeError fallback path uses the already-generated raw_audio, but the main path ignores it. After removing the double-synthesis, ensure both the primary and fallback paths use the same single synthesized WAV so behavior is consistent across VoiceDNAProcessor versions.

Copilot uses AI. Check for mistakes.
Comment on lines +131 to +138
try:
mapping: Dict[str, str] = json.loads(raw)
except json.JSONDecodeError as exc:
logger.warning("VOICEDNA_OPENCLAW_PRESETS_MAP is not valid JSON: %s", exc)
return AGENT_PRESETS

for agent_key, preset_name in mapping.items():
if preset_name not in PRESET_REGISTRY:

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

load_presets_from_env() assumes VOICEDNA_OPENCLAW_PRESETS_MAP decodes to a dict[str, str]. If the env var is valid JSON but not an object (e.g., a list/null/number), mapping.items() will raise at runtime. Add a type check after json.loads (and ideally validate keys/values are strings) to avoid crashing the OpenClaw hook on misconfiguration.

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +75
def _get_adapter() -> VoiceAdapter:
"""Return (and lazily create) the module-level VoiceAdapter instance."""
global _adapter # noqa: PLW0603
if _adapter is None:
# Populate from env first; fall back to pilot defaults
env_map = load_presets_from_env()
agent_presets = dict(_DEFAULT_PILOT_MAP)
agent_presets.update(env_map)
_adapter = VoiceAdapter(agent_presets=agent_presets)
logger.debug(

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_get_adapter() lazily initializes a module-level singleton without any synchronization. If OpenClaw calls render_agent_voice() concurrently (common in async/web contexts), multiple threads could race to create adapters or observe a partially-initialized global. Consider guarding initialization with a threading.Lock or using functools.lru_cache for _get_adapter() to make this thread-safe.

Copilot uses AI. Check for mistakes.
Comment on lines +106 to +112
@pytest.mark.skipif(
not all([p.read_bytes() for p in [
Path(__file__).resolve().parents[1] / "examples" / "openclaw" / "output" / f
for f in ["namshub_neutral.wav", "david_friendly.wav", "voss_flair.wav"]
] if p.exists()]),
reason="Demo WAV files not all present"
)

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @skipif condition reads the full demo WAV contents at collection time (p.read_bytes()), which adds unnecessary I/O to every test run and can be slow/flaky on CI filesystems. Prefer checking only for file existence/size in the marker, and perform any content validation inside the test body (using pytest.skip when prerequisites aren’t met).

Copilot uses AI. Check for mistakes.
Comment thread README.md
Comment on lines +56 to +58
The feature is entirely additive and disabled by default. Enable it by:

1. Setting `VOICEDNA_OPENCLAW_PRESETS=1` in your environment (signals intent; not strictly required by the code).

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README says setting VOICEDNA_OPENCLAW_PRESETS=1 is “not strictly required by the code,” but the OpenClaw hook entrypoint render_agent_voice() explicitly returns None unless this env var is set. Suggest clarifying that the env var is required for the openclaw_live_voice hook path, while it’s optional when using VoiceAdapter directly.

Suggested change
The feature is entirely additive and disabled by default. Enable it by:
1. Setting `VOICEDNA_OPENCLAW_PRESETS=1` in your environment (signals intent; not strictly required by the code).
The feature is entirely additive and disabled by default. Enable it by:
1. Setting `VOICEDNA_OPENCLAW_PRESETS=1` in your environment. This is required when using the OpenClaw hook path (`openclaw_live_voice` / `render_agent_voice()`), and optional only if you are instantiating `VoiceAdapter` directly.

Copilot uses AI. Check for mistakes.
…o WAVs

- DESIGN_DOC.md: add David's 5-item edge-case validation checklist
- Refresh demo WAVs (namshub_neutral, david_friendly, voss_flair)
- Add research prep artifacts: .env.example, openclaw_integration_plan.md,
  research_summary.md, smoke_test_tts.sh, prep_for_voss/checklist.md
- 52 tests pass, ruff clean
@lukejmorrison
lukejmorrison merged commit c1d157f into main Apr 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants