from __future__ import annotations import queue import subprocess import sys import threading from typing import Any from loguru import logger class RepAnnouncer: """运动次数语音播报器""" def __init__(self, *, enabled: bool = True, rate: int = 185, volume: float = 1.0) -> None: """初始化TTS引擎(macOS用say,其他系统用pyttsx3)""" self.enabled = enabled self.rate = rate self.volume = volume self._queue: queue.Queue[str | None] = queue.Queue() self._thread: threading.Thread | None = None self._engine: Any | None = None self._use_macos_say = False self._current_process: subprocess.Popen | None = None if self.enabled: self._start() def announce_count(self, count: int) -> None: """将次数放入队列进行异步语音播报""" if not self.enabled or count <= 0: return while True: try: self._queue.get_nowait() except queue.Empty: break self._queue.put(str(count)) def close(self) -> None: """停止播报线程并释放资源""" if not self.enabled: return self._queue.put(None) if self._thread is not None: self._thread.join(timeout=1.0) if self._current_process is not None and self._current_process.poll() is None: self._current_process.terminate() def _start(self) -> None: """根据平台初始化TTS引擎并启动后台播报线程""" if sys.platform == "darwin": self._use_macos_say = True logger.info("Rep announcer initialized with macOS say") else: try: import pyttsx3 self._engine = pyttsx3.init() self._engine.setProperty("rate", self.rate) self._engine.setProperty("volume", self.volume) logger.info("Rep announcer initialized with pyttsx3") except Exception as exc: self.enabled = False logger.warning("Rep announcer disabled, pyttsx3 unavailable: {}", exc) return self._thread = threading.Thread(target=self._run, name="RepAnnouncer", daemon=True) self._thread.start() def _run(self) -> None: """后台线程:从队列读取文本并调用TTS播放""" while True: text = self._queue.get() if text is None: return try: if self._use_macos_say: if self._current_process is not None and self._current_process.poll() is None: self._current_process.terminate() self._current_process = subprocess.Popen( ["say", "-r", str(self.rate), text], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) elif self._engine is not None: self._engine.say(text) self._engine.runAndWait() except Exception as exc: logger.warning("Failed to announce rep count {}: {}", text, exc)