File size: 2,722 Bytes
dc98a34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Literal, Optional
Provenance = Literal['real_checkpoint', 'trained_by_us', 'rule', 'architecture_reimplemented', 'unavailable']
GateChoice = Literal['webrtcvad', 'silero_vad', 'none']
EncoderChoice = Literal['whisper_tiny', 'whisper_base', 'wav2vec2', 'smart_turn_onnx', 'none']
PoolingChoice = Literal['mean', 'cross_attention']
HeadChoice = Literal['linear', 'mlp']
SemanticChoice = Literal['off', 'qwen_local', 'qwen_local_streaming', 'livekit_eou', 'groq_api', 'openrouter_api']
FusionChoice = Literal['off', 'weighted_vote', 'easy_turn']
OutputClasses = Literal['binary', '3class', '4class']
FullDuplexChoice = Literal['off', 'moshi', 'human1']

class StageUnavailableError(RuntimeError):
    pass

@dataclass
class StageResult:
    stage: str
    timing_ms: float
    output: Any = None
    available: bool = True
    reason: Optional[str] = None
    provenance: Optional[Provenance] = None

@dataclass
class PipelineConfig:
    gate: GateChoice = 'silero_vad'
    encoder: EncoderChoice = 'whisper_tiny'
    pooling: PoolingChoice = 'mean'
    head: HeadChoice = 'linear'
    semantic: SemanticChoice = 'off'
    fusion: FusionChoice = 'off'
    output_classes: OutputClasses = 'binary'
    full_duplex: FullDuplexChoice = 'off'
    vad_aggressiveness: int = 2
    silence_trigger_ms: int = 400
    semantic_temperature: float = 0.2
    acoustic_weight: float = 0.6

    def __post_init__(self) -> None:
        if self.encoder == 'none':
            if self.semantic == 'off':
                raise ValueError("encoder='none' requires the semantic branch to be on (nothing would decide).")
            if self.fusion != 'off':
                raise ValueError("encoder='none' has no acoustic score to fuse with - set fusion='off'.")
        if self.encoder == 'smart_turn_onnx':
            if self.pooling != 'mean' or self.head != 'linear':
                raise ValueError("encoder='smart_turn_onnx' is a single opaque preset and cannot be combined with a separate pooling/head choice (see docs/decision-log.md #18).")
        if self.fusion != 'off' and self.semantic == 'off':
            raise ValueError('fusion requires the semantic branch to be on.')
        if self.full_duplex != 'off':
            pass

@dataclass
class PipelineResult:
    config: PipelineConfig
    decision: Optional[str] = None
    probability: Optional[float] = None
    transcript: Optional[str] = None
    semantic_verdict: Optional[str] = None
    stage_results: list[StageResult] = field(default_factory=list)
    total_latency_ms: float = 0.0
    provenance: Optional[Provenance] = None
    preset_label: Optional[str] = None