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>
This commit is contained in:
parent
766f4aae2b
commit
cb0deb5f9d
15 changed files with 1359 additions and 24 deletions
|
|
@ -25,7 +25,7 @@ from abc import ABC, abstractmethod
|
|||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
import httpx
|
||||
|
|
@ -77,7 +77,7 @@ class SkillMeta:
|
|||
class SkillBundle:
|
||||
"""A downloaded skill ready for quarantine/scanning/installation."""
|
||||
name: str
|
||||
files: Dict[str, str] # relative_path -> text content
|
||||
files: Dict[str, Union[str, bytes]] # relative_path -> file content
|
||||
source: str
|
||||
identifier: str
|
||||
trust_level: str
|
||||
|
|
@ -1940,13 +1940,18 @@ class OptionalSkillSource(SkillSource):
|
|||
else:
|
||||
skill_dir = resolved
|
||||
|
||||
files: Dict[str, str] = {}
|
||||
files: Dict[str, Union[str, bytes]] = {}
|
||||
for f in skill_dir.rglob("*"):
|
||||
if f.is_file() and not f.name.startswith("."):
|
||||
if (
|
||||
f.is_file()
|
||||
and not f.name.startswith(".")
|
||||
and "__pycache__" not in f.parts
|
||||
and f.suffix != ".pyc"
|
||||
):
|
||||
rel_path = str(f.relative_to(skill_dir))
|
||||
try:
|
||||
files[rel_path] = f.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeDecodeError):
|
||||
files[rel_path] = f.read_bytes()
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
if not files:
|
||||
|
|
@ -2257,7 +2262,10 @@ def quarantine_bundle(bundle: SkillBundle) -> Path:
|
|||
for rel_path, file_content in bundle.files.items():
|
||||
file_dest = dest / rel_path
|
||||
file_dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
file_dest.write_text(file_content, encoding="utf-8")
|
||||
if isinstance(file_content, bytes):
|
||||
file_dest.write_bytes(file_content)
|
||||
else:
|
||||
file_dest.write_text(file_content, encoding="utf-8")
|
||||
|
||||
return dest
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue