Files
reyna-cli/src/reyna_cli/speech_execution.py
T
2026-08-16 08:24:45 -04:00

114 lines
4.9 KiB
Python

"""Direct Apple SpeechTranscriber file execution for Reyna CLI."""
from __future__ import annotations
import hashlib
import json
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import Any, Dict, Optional
_SWIFT_SOURCE = r'''import Speech
import AVFoundation
import Foundation
@main
struct ReynaTranscriber {
static func main() async {
let args = CommandLine.arguments
guard args.count >= 2 else { fail("audio path is required", code: 2) }
let audioPath = args[1]
let localeID = args.count >= 3 ? args[2] : "en-US"
guard FileManager.default.fileExists(atPath: audioPath) else { fail("audio file not found", code: 3) }
guard SpeechTranscriber.isAvailable else { fail("SpeechTranscriber is unavailable", code: 4) }
let requested = Locale(identifier: localeID)
guard let locale = await SpeechTranscriber.supportedLocale(equivalentTo: requested) else { fail("unsupported locale", code: 5) }
let transcriber = SpeechTranscriber(locale: locale, preset: .transcription)
do {
if let request = try await AssetInventory.assetInstallationRequest(supporting: [transcriber]) {
try await request.downloadAndInstall()
}
let file = try AVAudioFile(forReading: URL(fileURLWithPath: audioPath))
let analyzer = try await SpeechAnalyzer(inputAudioFile: file, modules: [transcriber], finishAfterFile: true)
_ = analyzer
var segments: [String] = []
for try await result in transcriber.results {
let text = String(result.text.characters).trimmingCharacters(in: .whitespacesAndNewlines)
if !text.isEmpty { segments.append(text) }
}
let payload: [String: Any] = [
"ok": true,
"engine": "SpeechAnalyzer+SpeechTranscriber",
"locale": locale.identifier,
"requestedLocale": localeID,
"transcript": segments.joined(separator: " "),
"segments": segments,
"audioPath": audioPath,
"isAvailable": SpeechTranscriber.isAvailable
]
let data = try JSONSerialization.data(withJSONObject: payload, options: [.sortedKeys])
FileHandle.standardOutput.write(data)
} catch {
fail(String(describing: error), code: 6)
}
}
static func fail(_ message: String, code: Int32) -> Never {
let payload: [String: Any] = ["ok": false, "error": message]
if let data = try? JSONSerialization.data(withJSONObject: payload, options: []) { FileHandle.standardOutput.write(data) }
exit(code)
}
}
'''
class SpeechExecutionError(RuntimeError):
pass
def _cache_dir() -> Path:
return Path.home() / "Library" / "Application Support" / "reyna-cli" / "speech"
def _binary_path() -> Path:
digest = hashlib.sha256(_SWIFT_SOURCE.encode()).hexdigest()[:16]
return _cache_dir() / f"transcriber-{digest}"
def _ensure_binary() -> Path:
cached = _binary_path()
if cached.exists() and os.access(cached, os.X_OK):
return cached
swiftc = shutil.which("swiftc") or "/usr/bin/swiftc"
if not Path(swiftc).exists():
raise SpeechExecutionError("swiftc is required for SpeechTranscriber file execution")
_cache_dir().mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix="reyna-speech-build-") as tmp:
source = Path(tmp) / "Transcriber.swift"
binary = Path(tmp) / "transcriber"
source.write_text(_SWIFT_SOURCE, encoding="utf-8")
result = subprocess.run([swiftc, "-O", "-parse-as-library", str(source), "-o", str(binary), "-framework", "AVFoundation", "-framework", "Speech"], capture_output=True, text=True, timeout=120, check=False)
if result.returncode != 0:
raise SpeechExecutionError(f"swiftc failed: {(result.stderr or result.stdout).strip()[:2000]}")
shutil.copyfile(binary, cached)
cached.chmod(0o700)
return cached
def transcribe_file(audio_path: Path, *, locale: str = "en-US", timeout: int = 300) -> Dict[str, Any]:
audio = audio_path.expanduser().resolve()
if not audio.is_file():
raise SpeechExecutionError(f"audio file not found: {audio}")
binary = _ensure_binary()
result = subprocess.run([str(binary), str(audio), locale], capture_output=True, text=True, timeout=timeout, check=False)
raw = (result.stdout or "").strip()
try:
payload = json.loads(raw) if raw else {}
except json.JSONDecodeError as exc:
raise SpeechExecutionError(f"SpeechTranscriber returned invalid JSON: {raw[:500]}") from exc
if result.returncode != 0 or not payload.get("ok"):
raise SpeechExecutionError(str(payload.get("error") or result.stderr.strip() or "SpeechTranscriber failed"))
return {**payload, "source": "reyna_cli_direct", "binary": str(binary)}