FlyBrain-Lab / src /tools /speech.py
timfromhcs's picture
FlyBrain V5: src/tools/speech.py (optional-dep guards, v1, backup)
49b7760 verified
Raw
History Blame Contribute Delete
3.43 kB
import os
import subprocess
try:
import soundfile as sf
except ImportError: # minimal hosts (e.g. HF Space): TTS/file synthesis disabled
sf = None
import numpy as np
from typing import Dict, Any
from src.tools.base import ToolConnector
class SpeakConnector(ToolConnector):
def __init__(self, audio_dir: str = "visual_evidence/audio"):
super().__init__(
name="speak",
description="Synthesizes and speaks text audio using local Windows SAPI TTS and records WAV artifact.",
timeout_sec=15.0
)
self.audio_dir = audio_dir
os.makedirs(self.audio_dir, exist_ok=True)
@property
def input_schema(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"text": {"type": "string"},
"play_sound": {"type": "boolean", "default": False}
},
"required": ["text"]
}
@property
def output_schema(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"text_spoken": {"type": "string"},
"wav_file": {"type": "string"},
"duration_sec": {"type": "number"},
"rms_energy": {"type": "number"}
},
"required": ["text_spoken", "wav_file", "duration_sec", "rms_energy"]
}
def _execute(self, params: Dict[str, Any], execution_id: str) -> Dict[str, Any]:
text = str(params["text"])
wav_filename = f"speech_{execution_id[:8]}.wav"
wav_path = os.path.abspath(os.path.join(self.audio_dir, wav_filename))
# Attempt Windows PowerShell SAPI script if on Windows
if os.name == "nt":
ps_script = f"""
Add-Type -AssemblyName System.Speech
$synth = New-Object System.Speech.Synthesis.SpeechSynthesizer
$synth.SetOutputToWaveFile('{wav_path}')
$synth.Speak('{text}')
$synth.Dispose()
"""
try:
subprocess.run(
["powershell", "-NoProfile", "-Command", ps_script],
capture_output=True,
text=True,
timeout=self.timeout_sec
)
except Exception:
pass
if not os.path.exists(wav_path):
if sf is None:
raise RuntimeError("soundfile unavailable on this host: "
"speech synthesis disabled (no fake audio generated)")
# Pure Python acoustic audio synthesis fallback for Linux / headless CI
sr = 22050
duration = max(0.6, len(text) * 0.05)
t = np.linspace(0, duration, int(sr * duration), endpoint=False)
f0 = 220.0 + 40.0 * np.sin(2 * np.pi * 3.0 * t)
phase = 2 * np.pi * np.cumsum(f0) / sr
envelope = np.clip(np.sin(np.pi * t / duration), 0, 1) ** 0.5
carrier = np.sin(phase) + 0.3 * np.sin(2 * phase)
audio = (carrier * envelope * 0.4).astype(np.float32)
sf.write(wav_path, audio, sr)
# Measure generated audio
data, sr = sf.read(wav_path)
duration = float(len(data) / sr)
rms = float(np.sqrt(np.mean(data**2)))
return {
"text_spoken": text,
"wav_file": wav_path,
"duration_sec": round(duration, 3),
"rms_energy": round(rms, 4)
}