[OpenSpec 与项目骨架]:完成实施型变更与 Python 基础骨架,包含 OpenSpec 工件、核心模型、配置边界和基础测试
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, Mapping
|
||||
|
||||
|
||||
class PipelineState(str, Enum):
|
||||
IDLE = "idle"
|
||||
WAKE_LISTENING = "wake_listening"
|
||||
SPEECH_DETECTING = "speech_detecting"
|
||||
RECORDING = "recording"
|
||||
TRANSCRIBING = "transcribing"
|
||||
THINKING = "thinking"
|
||||
SPEAKING = "speaking"
|
||||
INTERRUPTED = "interrupted"
|
||||
ERROR_RECOVERING = "error_recovering"
|
||||
|
||||
|
||||
class ErrorCode(str, Enum):
|
||||
AUDIO_INPUT_DEVICE_MISSING = "AUDIO_INPUT_DEVICE_MISSING"
|
||||
AUDIO_OUTPUT_DEVICE_MISSING = "AUDIO_OUTPUT_DEVICE_MISSING"
|
||||
AUDIO_PERMISSION_DENIED = "AUDIO_PERMISSION_DENIED"
|
||||
AUDIO_STREAM_UNDERRUN = "AUDIO_STREAM_UNDERRUN"
|
||||
AUDIO_FORMAT_UNSUPPORTED = "AUDIO_FORMAT_UNSUPPORTED"
|
||||
CONFIG_MISSING_VALUE = "CONFIG_MISSING_VALUE"
|
||||
WAKE_MODEL_MISSING = "WAKE_MODEL_MISSING"
|
||||
WAKE_MODEL_LOAD_FAILED = "WAKE_MODEL_LOAD_FAILED"
|
||||
WAKE_AUDIO_FORMAT_INVALID = "WAKE_AUDIO_FORMAT_INVALID"
|
||||
VAD_MODEL_LOAD_FAILED = "VAD_MODEL_LOAD_FAILED"
|
||||
VAD_TIMEOUT_NO_SPEECH = "VAD_TIMEOUT_NO_SPEECH"
|
||||
VAD_MAX_RECORDING_REACHED = "VAD_MAX_RECORDING_REACHED"
|
||||
STT_MODEL_MISSING = "STT_MODEL_MISSING"
|
||||
STT_TRANSCRIBE_FAILED = "STT_TRANSCRIBE_FAILED"
|
||||
STT_EMPTY_TRANSCRIPT = "STT_EMPTY_TRANSCRIPT"
|
||||
LLM_API_KEY_MISSING = "LLM_API_KEY_MISSING"
|
||||
LLM_REQUEST_TIMEOUT = "LLM_REQUEST_TIMEOUT"
|
||||
LLM_RATE_LIMITED = "LLM_RATE_LIMITED"
|
||||
LLM_NETWORK_ERROR = "LLM_NETWORK_ERROR"
|
||||
LLM_EMPTY_REPLY = "LLM_EMPTY_REPLY"
|
||||
TTS_MODEL_MISSING = "TTS_MODEL_MISSING"
|
||||
TTS_SYNTHESIS_FAILED = "TTS_SYNTHESIS_FAILED"
|
||||
TTS_EMPTY_AUDIO = "TTS_EMPTY_AUDIO"
|
||||
ASSET_MISSING = "ASSET_MISSING"
|
||||
VALIDATION_FAILED = "VALIDATION_FAILED"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ProviderError(Exception):
|
||||
code: ErrorCode
|
||||
message: str
|
||||
retryable: bool
|
||||
provider: str
|
||||
stage: str
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.code.value} [{self.stage}/{self.provider}]: {self.message}"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AudioFrame:
|
||||
pcm: bytes
|
||||
sample_rate: int
|
||||
channels: int
|
||||
timestamp_ms: int
|
||||
frame_id: int
|
||||
metadata: Mapping[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.sample_rate <= 0:
|
||||
raise ValueError("sample_rate must be positive")
|
||||
if self.channels <= 0:
|
||||
raise ValueError("channels must be positive")
|
||||
if self.timestamp_ms < 0:
|
||||
raise ValueError("timestamp_ms must be non-negative")
|
||||
if self.frame_id < 0:
|
||||
raise ValueError("frame_id must be non-negative")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AudioSegment:
|
||||
pcm: bytes
|
||||
sample_rate: int
|
||||
channels: int
|
||||
start_time_ms: int
|
||||
end_time_ms: int
|
||||
metadata: Mapping[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.sample_rate <= 0:
|
||||
raise ValueError("sample_rate must be positive")
|
||||
if self.channels <= 0:
|
||||
raise ValueError("channels must be positive")
|
||||
if self.start_time_ms < 0:
|
||||
raise ValueError("start_time_ms must be non-negative")
|
||||
if self.end_time_ms < self.start_time_ms:
|
||||
raise ValueError("end_time_ms must be >= start_time_ms")
|
||||
|
||||
@property
|
||||
def duration_ms(self) -> int:
|
||||
return self.end_time_ms - self.start_time_ms
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class WakeEvent:
|
||||
keyword: str
|
||||
confidence: float
|
||||
timestamp_ms: int
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class VadResult:
|
||||
is_speech: bool
|
||||
confidence: float
|
||||
speech_ms: int
|
||||
silence_ms: int
|
||||
end_reason: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Transcript:
|
||||
text: str
|
||||
language: str
|
||||
confidence: float | None
|
||||
duration_ms: int
|
||||
provider: str
|
||||
raw_metadata: Mapping[str, Any] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def normalized_text(self) -> str:
|
||||
return self.text.strip()
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Message:
|
||||
role: str
|
||||
content: str
|
||||
created_at: float
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ReplyDelta:
|
||||
text_delta: str
|
||||
is_sentence_boundary: bool = False
|
||||
finish_reason: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PlaybackResult:
|
||||
played: bool
|
||||
duration_ms: int
|
||||
error: ProviderError | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TransportHealth:
|
||||
input_available: bool
|
||||
output_available: bool
|
||||
message: str = ""
|
||||
Reference in New Issue
Block a user