项目文件夹

文件
2026-07-13 12:28:27 +08:00

182 行
5.3 KiB
Python

# coding: utf-8
"""
客户端状态管理模块
提供 ClientState 类用于管理客户端的全局状态。
使用 dataclass 提供类型安全和清晰的状态定义。
"""
from __future__ import annotations
import asyncio
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Optional, Dict, Any
if TYPE_CHECKING:
import sounddevice as sd
from websockets.legacy.client import WebSocketClientProtocol
from .app import CapsWriterClient
from rich.console import Console
from rich.theme import Theme
from . import logger
# 配置 Rich console
_theme = Theme({
'markdown.code': 'cyan',
'markdown.item.number': 'yellow'
})
console = Console(highlight=False, soft_wrap=True, theme=_theme)
@dataclass
class ClientState:
"""
客户端运行状态
管理客户端运行过程中的所有共享状态,包括事件循环、消息队列、
WebSocket 连接、音频流和录音状态等。
Attributes:
loop: asyncio 事件循环
queue_in: 音频数据输入队列
queue_out: 处理结果输出队列(保留)
websocket: WebSocket 客户端连接
stream: 音频输入流
recording: 是否正在录音
recording_start_time: 录音开始时间戳
audio_files: 任务ID到音频文件路径的映射
last_recognition_text: 最近一次识别的最终文本(热词替换后),供"添加纠错记录"使用
"""
queue_in: asyncio.Queue = field(default_factory=asyncio.Queue)
queue_out: asyncio.Queue = field(default_factory=asyncio.Queue)
websocket: Optional[WebSocketClientProtocol] = None
stream: Optional[sd.InputStream] = None
app: Optional[CapsWriterClient] = None
recording: bool = False
recording_start_time: float = 0.0
audio_files: Dict[str, Path] = field(default_factory=dict)
# 最近一次识别结果(用于手动添加纠错记录)
last_recognition_text: Optional[str] = None
# 最近一次输出内容(如果是 LLM 润色,则是润色结果;否则是原始识别结果)
last_output_text: Optional[str] = None
def reset(self) -> None:
"""
重置状态
清理所有状态,关闭连接和流。用于重新初始化或退出时清理。
"""
logger.debug("正在重置客户端状态...")
# 关闭 WebSocket 连接
ws = self.websocket
if ws is not None:
try:
if not ws.closed and self.app and self.app.loop and self.app.loop.is_running():
asyncio.run_coroutine_threadsafe(ws.close(), self.app.loop)
except Exception:
pass
self.websocket = None
# 关闭音频流
if self.stream is not None:
try:
self.stream.close()
logger.debug("音频流已关闭")
except Exception:
pass
self.stream = None
# 重置其他状态
self.recording = False
self.recording_start_time = 0.0
self.audio_files.clear()
logger.debug("客户端状态重置完成")
def start_recording(self, start_time: float) -> None:
"""
开始录音
Args:
start_time: 录音开始的时间戳
"""
self.recording = True
self.recording_start_time = start_time
logger.debug(f"录音状态已更新: recording=True, start_time={start_time:.2f}")
def stop_recording(self) -> float:
"""
停止录音
Returns:
录音持续时间(秒)
"""
duration = 0.0
if self.recording_start_time > 0:
duration = time.time() - self.recording_start_time
self.recording = False
self.recording_start_time = 0.0
logger.debug(f"录音状态已更新: recording=False, duration={duration:.2f}s")
return duration
@property
def is_connected(self) -> bool:
"""检查 WebSocket 是否已连接"""
if self.websocket is None:
return False
try:
return not self.websocket.closed
except AttributeError:
return self.websocket is not None
def register_audio_file(self, task_id: str, file_path: Path) -> None:
"""
注册音频文件
Args:
task_id: 任务ID
file_path: 音频文件路径
"""
self.audio_files[task_id] = file_path
logger.debug(f"注册音频文件: task_id={task_id}, path={file_path}")
def pop_audio_file(self, task_id: str) -> Optional[Path]:
"""
获取并移除音频文件路径
Args:
task_id: 任务ID
Returns:
音频文件路径,如果不存在则返回 None
"""
file_path = self.audio_files.pop(task_id, None)
if file_path:
logger.debug(f"获取音频文件: task_id={task_id}, path={file_path}")
return file_path
def set_output_text(self, text: str) -> None:
"""
设置最近一次输出文本
Args:
text: 输出文本内容
"""
self.last_output_text = text