wangzhibo
2026-08-30 c8515170fd399b4d2dfaebb59128d4674e58915e
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
from dataclasses import dataclass
from pathlib import Path
 
from ..config import RuntimeConfig
 
REQUIRED_MODEL_FILES = (
    "encoder.onnx",
    "decoder.onnx",
    "joiner.onnx",
    "tokens.txt",
)
 
 
@dataclass
class DetectorAssets:
    model_root: Path
    tokens_file: Path
    encoder_file: Path
    decoder_file: Path
    joiner_file: Path
    keywords_file: Path
 
 
class DetectorAssetsBuilder:
    def __init__(self, config: RuntimeConfig) -> None:
        self.config = config
 
    def prepare(self) -> DetectorAssets:
        model_root = self._resolve_model_root()
        keywords_file = self._write_keywords_file(model_root)
        return DetectorAssets(
            model_root=model_root,
            tokens_file=model_root / "tokens.txt",
            encoder_file=model_root / "encoder.onnx",
            decoder_file=model_root / "decoder.onnx",
            joiner_file=model_root / "joiner.onnx",
            keywords_file=keywords_file,
        )
 
    def _resolve_model_root(self) -> Path:
        preferred = self.config.model_dir
        if self._has_required_files(preferred):
            return preferred
 
        raise FileNotFoundError(
            "No valid model directory found. Expected configured model files to exist."
        )
 
    def _write_keywords_file(self, model_root: Path) -> Path:
        try:
            from pypinyin import Style, pinyin
        except ImportError as exc:
            raise RuntimeError(
                "Missing dependency: pypinyin. Install runtime dependencies before generating keywords."
            ) from exc
 
        wake_words = self.config.wake_words
        if not wake_words:
            raise ValueError("keywords.txt cannot be empty")
 
        keywords_path = model_root / "keywords.txt"
        lines: list[str] = []
        for keyword_text in wake_words:
            initials = pinyin(keyword_text, style=Style.INITIALS, strict=False)
            finals = pinyin(
                keyword_text,
                style=Style.FINALS_TONE,
                strict=False,
                neutral_tone_with_five=True,
            )
 
            tokens: list[str] = []
            for initial_parts, final_parts in zip(initials, finals):
                initial = initial_parts[0].strip()
                final = final_parts[0].strip()
                if initial:
                    tokens.append(initial)
                if final:
                    tokens.append(final)
 
            if not tokens:
                raise ValueError(
                    f"failed to generate pinyin tokens for wake word: {keyword_text}"
                )
 
            lines.append(f"{' '.join(tokens)} @{keyword_text}")
 
        keywords_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
        return keywords_path
 
    def _has_required_files(self, directory: Path) -> bool:
        if not directory.exists():
            return False
        return all((directory / file_name).exists() for file_name in REQUIRED_MODEL_FILES)