# app/audio/generate.py from __future__ import annotations import platform import shutil import subprocess from pathlib import Path from loguru import logger def generate_rep_audio_files( *, max_count: int, rate: int, output_dir: Path, overwrite: bool = False, ) -> None: """ 确保 0~max_count 的运动次数语音 wav 文件存在。 默认生成到: app/audio/reps/0.wav app/audio/reps/1.wav ... app/audio/reps/200.wav 服务启动时调用一次即可。 """ output_dir.mkdir(parents=True, exist_ok=True) missing_counts = [ count for count in range(0, max_count + 1) if overwrite or not _audio_path(output_dir, count).exists() ] if not missing_counts: logger.info("Rep audio files already prepared: {}", output_dir) return system = platform.system().lower() logger.info( "Preparing rep audio files, system={}, count={}, output_dir={}", system, len(missing_counts), output_dir, ) if system == "darwin": _generate_with_macos_say( counts=missing_counts, output_dir=output_dir, rate=rate, ) else: _generate_with_pyttsx3( counts=missing_counts, output_dir=output_dir, rate=rate, ) logger.info("Rep audio files prepared: {}", output_dir) def _generate_with_macos_say( *, counts: list[int], output_dir: Path, rate: int, ) -> None: """macOS 使用 say 命令生成 wav。""" if platform.system().lower() != "darwin": raise RuntimeError("say command is only available on macOS") if shutil.which("say") is None: raise RuntimeError("macOS say command not found") for count in counts: audio_file = _audio_path(output_dir, count) subprocess.run( [ "say", "-r", str(rate), "--file-format=WAVE", "-o", str(audio_file), str(count), ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True, ) def _generate_with_pyttsx3( *, counts: list[int], output_dir: Path, rate: int, ) -> None: """Windows / Linux 使用 pyttsx3 生成 wav。""" try: import pyttsx3 except Exception as exc: raise RuntimeError(f"pyttsx3 unavailable: {exc}") from exc engine = pyttsx3.init() engine.setProperty("rate", rate) engine.setProperty("volume", 1.0) for count in counts: audio_file = _audio_path(output_dir, count) engine.save_to_file(str(count), str(audio_file)) engine.runAndWait() def _audio_path(output_dir: Path, count: int) -> Path: return output_dir / f"{count}.wav"