Files
hermes-agent/optional-skills/mlops/models/neutts/scripts/bootstrap_neutts_cli.py
Teknium cb0deb5f9d feat: add NeuTTS optional skill + local TTS provider backend
* feat(skills): add bundled neutts optional skill

Add NeuTTS optional skill with CLI scaffold, bootstrap helper, and
sample voice profile. Also fixes skills_hub.py to handle binary
assets (WAV files) during skill installation.

Changes:
- optional-skills/mlops/models/neutts/ — skill + CLI scaffold
- tools/skills_hub.py — binary asset support (read_bytes, write_bytes)
- tests/tools/test_skills_hub.py — regression tests for binary assets

* feat(tts): add NeuTTS as local TTS provider backend

Add NeuTTS as a fourth TTS provider option alongside Edge, ElevenLabs,
and OpenAI. NeuTTS runs fully on-device via neutts_cli — no API key
needed.

Provider behavior:
- Explicit: set tts.provider to 'neutts' in config.yaml
- Fallback: when Edge TTS is unavailable and neutts_cli is installed,
  automatically falls back to NeuTTS instead of failing
- check_tts_requirements() now includes NeuTTS in availability checks

NeuTTS outputs WAV natively. For Telegram voice bubbles, ffmpeg
converts to Opus (same pattern as Edge TTS).

Changes:
- tools/tts_tool.py — _generate_neutts(), _check_neutts_available(),
  provider dispatch, fallback logic, Opus conversion
- hermes_cli/config.py — tts.neutts config defaults

---------

Co-authored-by: unmodeled-tyler <unmodeled.tyler@proton.me>
2026-03-17 02:13:34 -07:00

169 lines
5.1 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import shlex
import subprocess
import sys
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
SKILL_DIR = SCRIPT_DIR.parent
BUNDLED_CLI_DIR = SKILL_DIR / "assets" / "neutts-cli"
def _quote(path: Path) -> str:
return shlex.quote(str(path))
def _quote_text(value: str) -> str:
return shlex.quote(value)
def find_cli_dir() -> tuple[Path, str]:
if BUNDLED_CLI_DIR.exists():
return BUNDLED_CLI_DIR, "bundled"
raise FileNotFoundError(
"NeuTTS CLI scaffold not found in bundled skill assets."
)
def build_commands(
cli_dir: Path,
install_cli: bool,
sample_profile: bool,
python_executable: str,
) -> list[str]:
commands: list[str] = []
module_runner = f"{_quote_text(python_executable)} -m neutts_cli.cli"
if install_cli:
commands.append(
f"{_quote_text(python_executable)} -m pip install --no-build-isolation -e {_quote(cli_dir)}"
)
commands.append(f"{module_runner} doctor")
else:
commands.append("neutts doctor")
if sample_profile:
sample_audio = cli_dir / "samples" / "jo.wav"
sample_text = cli_dir / "samples" / "jo.txt"
if not sample_audio.exists() or not sample_text.exists():
raise FileNotFoundError(
"Sample profile files are missing from bundled skill assets."
)
commands.append(
" ".join(
[
f"{module_runner if install_cli else 'neutts'} add-voice jo-demo",
f"--ref-audio {_quote(sample_audio)}",
f"--ref-text-file {_quote(sample_text)}",
"--language en",
]
)
)
return commands
def maybe_run(commands: list[str], workdir: Path, execute: bool) -> list[dict]:
results: list[dict] = []
for command in commands:
if not execute:
results.append({"command": command, "executed": False})
continue
completed = subprocess.run(
shlex.split(command),
cwd=str(workdir),
text=True,
capture_output=True,
check=False,
)
results.append(
{
"command": command,
"executed": True,
"returncode": completed.returncode,
"stdout": completed.stdout.strip(),
"stderr": completed.stderr.strip(),
}
)
if completed.returncode != 0:
break
return results
def main() -> int:
parser = argparse.ArgumentParser(
description="Bootstrap the standalone NeuTTS CLI for Hermes skill usage"
)
parser.add_argument(
"--repo-root",
default=".",
help="Working directory used when executing bootstrap commands",
)
parser.add_argument(
"--install-cli",
action="store_true",
help="Install the standalone NeuTTS CLI in editable mode",
)
parser.add_argument(
"--sample-profile",
action="store_true",
help="Add the bundled jo-demo sample profile",
)
parser.add_argument(
"--execute", action="store_true", help="Actually run the generated commands"
)
parser.add_argument(
"--json", action="store_true", help="Print machine-readable JSON output"
)
args = parser.parse_args()
repo_root = Path(args.repo_root).expanduser().resolve()
cli_dir, cli_source = find_cli_dir()
commands = build_commands(
cli_dir, args.install_cli, args.sample_profile, sys.executable
)
workdir = repo_root if repo_root.exists() else Path.cwd()
results = maybe_run(commands, workdir, args.execute)
payload = {
"python_executable": sys.executable,
"repo_root": str(repo_root),
"workdir": str(workdir),
"cli_dir": str(cli_dir),
"cli_source": cli_source,
"commands": commands,
"results": results,
"next_steps": [
"Re-run with '--execute' to actually perform the bootstrap commands.",
f"Run '{sys.executable} -m neutts_cli.cli install --all' to install the upstream NeuTTS runtime.",
f"Run '{sys.executable} -m neutts_cli.cli list-voices' to confirm saved profiles.",
f"Run '{sys.executable} -m neutts_cli.cli synth --voice jo-demo --text Hello from Hermes' for a smoke test.",
],
}
if args.json:
print(json.dumps(payload, indent=2))
else:
print(f"Repo root: {repo_root}")
print(f"Workdir: {workdir}")
print(f"CLI dir: {cli_dir}")
print(f"CLI source: {cli_source}")
for entry in results:
print(f"- {entry['command']}")
if entry.get("executed"):
print(f" rc={entry['returncode']}")
if entry.get("stdout"):
print(f" stdout: {entry['stdout']}")
if entry.get("stderr"):
print(f" stderr: {entry['stderr']}")
for step in payload["next_steps"]:
print(f"next: {step}")
return 0
if __name__ == "__main__":
raise SystemExit(main())