utterverify 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- utterverify/__init__.py +30 -0
- utterverify/__main__.py +3 -0
- utterverify/_version.py +24 -0
- utterverify/bundle.py +646 -0
- utterverify/cli.py +406 -0
- utterverify/config.py +261 -0
- utterverify/core.py +419 -0
- utterverify/doctor.py +509 -0
- utterverify/errors.py +17 -0
- utterverify/py.typed +0 -0
- utterverify/types.py +102 -0
- utterverify-0.1.0.dist-info/METADATA +251 -0
- utterverify-0.1.0.dist-info/RECORD +17 -0
- utterverify-0.1.0.dist-info/WHEEL +5 -0
- utterverify-0.1.0.dist-info/entry_points.txt +2 -0
- utterverify-0.1.0.dist-info/licenses/LICENSE +201 -0
- utterverify-0.1.0.dist-info/top_level.txt +1 -0
utterverify/doctor.py
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
"""Side-effect-free readiness checks for the selected validation stack."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import os
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from dataclasses import asdict, dataclass, replace
|
|
9
|
+
from importlib.metadata import PackageNotFoundError
|
|
10
|
+
from importlib.metadata import version as package_version
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
from .config import CONFIG_FILENAME, UNSET, VerifyConfig, _UnsetType, default_config, load_config
|
|
15
|
+
from .errors import BundleError, ConfigError, UtterVerifyError
|
|
16
|
+
|
|
17
|
+
CheckStatus = Literal["pass", "warn", "fail"]
|
|
18
|
+
ModelSource = Literal["cli", "config", "environment", "none"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class DoctorCheck:
|
|
23
|
+
name: str
|
|
24
|
+
status: CheckStatus
|
|
25
|
+
detail: str
|
|
26
|
+
hint: str | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class DoctorResult:
|
|
31
|
+
ok: bool
|
|
32
|
+
backend: str
|
|
33
|
+
device: str
|
|
34
|
+
model: str | None
|
|
35
|
+
model_source: ModelSource
|
|
36
|
+
checks: tuple[DoctorCheck, ...]
|
|
37
|
+
input_error: bool = False
|
|
38
|
+
|
|
39
|
+
def to_dict(self) -> dict[str, object]:
|
|
40
|
+
"""Return the stable doctor JSON envelope."""
|
|
41
|
+
return {
|
|
42
|
+
"format": "utterverify.doctor",
|
|
43
|
+
"schema_version": 1,
|
|
44
|
+
"ok": self.ok,
|
|
45
|
+
"backend": self.backend,
|
|
46
|
+
"device": self.device,
|
|
47
|
+
"model": self.model,
|
|
48
|
+
"model_source": self.model_source,
|
|
49
|
+
"checks": [asdict(check) for check in self.checks],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def run_doctor(
|
|
54
|
+
*,
|
|
55
|
+
config_path: str | Path | None = None,
|
|
56
|
+
config: VerifyConfig | None = None,
|
|
57
|
+
backend: str | None = None,
|
|
58
|
+
device: str | None = None,
|
|
59
|
+
model: str | _UnsetType = UNSET,
|
|
60
|
+
) -> DoctorResult:
|
|
61
|
+
"""Check importability and local readiness without creating files or running ASR."""
|
|
62
|
+
checks: list[DoctorCheck] = []
|
|
63
|
+
input_error = False
|
|
64
|
+
selected_config = config
|
|
65
|
+
if selected_config is None and config_path is not None:
|
|
66
|
+
try:
|
|
67
|
+
selected_config = load_config(config_path)
|
|
68
|
+
checks.append(DoctorCheck("config", "pass", f"loaded {Path(config_path)}"))
|
|
69
|
+
except ConfigError as exc:
|
|
70
|
+
selected_config = default_config()
|
|
71
|
+
input_error = True
|
|
72
|
+
checks.append(DoctorCheck("config", "fail", str(exc)))
|
|
73
|
+
elif selected_config is not None:
|
|
74
|
+
detail = (
|
|
75
|
+
f"loaded {Path(config_path)}" if config_path is not None else "configuration is valid"
|
|
76
|
+
)
|
|
77
|
+
checks.append(DoctorCheck("config", "pass", detail))
|
|
78
|
+
else:
|
|
79
|
+
selected_config = default_config()
|
|
80
|
+
checks.append(DoctorCheck("config", "pass", "using built-in defaults"))
|
|
81
|
+
|
|
82
|
+
assert selected_config is not None
|
|
83
|
+
effective_backend = backend if backend is not None else selected_config.backend
|
|
84
|
+
effective_device = device if device is not None else selected_config.device
|
|
85
|
+
if isinstance(model, str):
|
|
86
|
+
effective_model = model
|
|
87
|
+
model_source: ModelSource = "cli"
|
|
88
|
+
elif selected_config.model is not None:
|
|
89
|
+
effective_model = selected_config.model
|
|
90
|
+
model_source = "config"
|
|
91
|
+
elif effective_backend == "whispercpp" and os.environ.get("SPEECHSCOPE_WHISPERCPP_MODEL"):
|
|
92
|
+
effective_model = os.environ["SPEECHSCOPE_WHISPERCPP_MODEL"]
|
|
93
|
+
model_source = "environment"
|
|
94
|
+
else:
|
|
95
|
+
effective_model = None
|
|
96
|
+
model_source = "none"
|
|
97
|
+
|
|
98
|
+
_check_renderplan(checks)
|
|
99
|
+
speechscope = _import_speechscope(checks)
|
|
100
|
+
backend_info = None
|
|
101
|
+
if speechscope is None:
|
|
102
|
+
checks.append(
|
|
103
|
+
DoctorCheck(
|
|
104
|
+
"backend",
|
|
105
|
+
"fail",
|
|
106
|
+
"SpeechScope is unavailable; backend metadata cannot be checked",
|
|
107
|
+
"Install the utterverify package dependencies.",
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
checks.append(DoctorCheck("device", "fail", "device support cannot be checked"))
|
|
111
|
+
else:
|
|
112
|
+
try:
|
|
113
|
+
backend_info = speechscope.backend_info(effective_backend)
|
|
114
|
+
except Exception as exc: # noqa: BLE001
|
|
115
|
+
checks.append(
|
|
116
|
+
DoctorCheck(
|
|
117
|
+
"backend",
|
|
118
|
+
"fail",
|
|
119
|
+
f"unknown or unavailable SpeechScope backend {effective_backend!r}: {exc}",
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
checks.append(DoctorCheck("device", "fail", "device support cannot be checked"))
|
|
123
|
+
else:
|
|
124
|
+
checks.append(
|
|
125
|
+
DoctorCheck(
|
|
126
|
+
"backend",
|
|
127
|
+
"pass",
|
|
128
|
+
f"{backend_info.name}"
|
|
129
|
+
+ (f" {backend_info.version}" if backend_info.version else ""),
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
supported = tuple(backend_info.supported_devices)
|
|
133
|
+
if effective_device in supported:
|
|
134
|
+
checks.append(DoctorCheck("device", "pass", effective_device))
|
|
135
|
+
else:
|
|
136
|
+
checks.append(
|
|
137
|
+
DoctorCheck(
|
|
138
|
+
"device",
|
|
139
|
+
"fail",
|
|
140
|
+
f"{effective_backend} does not support device {effective_device!r}; "
|
|
141
|
+
f"supported: {', '.join(supported)}",
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if speechscope is not None and backend_info is not None:
|
|
146
|
+
if backend_info.name == "whispercpp":
|
|
147
|
+
_check_whispercpp(speechscope, effective_model, model_source, checks)
|
|
148
|
+
elif backend_info.name == "redux":
|
|
149
|
+
_check_redux(backend_info, checks)
|
|
150
|
+
else:
|
|
151
|
+
checks.append(
|
|
152
|
+
DoctorCheck(
|
|
153
|
+
"backend_readiness",
|
|
154
|
+
"warn",
|
|
155
|
+
f"deep readiness checks are specific to SpeechScope backend {backend_info.name!r}",
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
ok = not input_error and all(check.status != "fail" for check in checks)
|
|
160
|
+
return DoctorResult(
|
|
161
|
+
ok=ok,
|
|
162
|
+
backend=effective_backend,
|
|
163
|
+
device=effective_device,
|
|
164
|
+
model=effective_model,
|
|
165
|
+
model_source=model_source,
|
|
166
|
+
checks=tuple(checks),
|
|
167
|
+
input_error=input_error,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def doctor_bundle(
|
|
172
|
+
bundle: str | Path,
|
|
173
|
+
*,
|
|
174
|
+
variant: str | None = None,
|
|
175
|
+
backend: str | None = None,
|
|
176
|
+
device: str | None = None,
|
|
177
|
+
model: str | _UnsetType = UNSET,
|
|
178
|
+
) -> DoctorResult:
|
|
179
|
+
"""Run doctor with a validated bundle's config and selected input handoffs."""
|
|
180
|
+
from .bundle import _contained_file, _load_bundle, _resolve_units, _resolve_variant
|
|
181
|
+
from .core import _load_handoff, _preflight_loaded
|
|
182
|
+
|
|
183
|
+
identity = _load_bundle(bundle)
|
|
184
|
+
selected_variant = _resolve_variant(identity, variant)
|
|
185
|
+
config_path = identity.root / "utterverify" / selected_variant / CONFIG_FILENAME
|
|
186
|
+
bundle_checks: list[DoctorCheck] = [
|
|
187
|
+
DoctorCheck("bundle_manifest", "pass", "canonical bundle manifest is valid"),
|
|
188
|
+
DoctorCheck("bundle_variant", "pass", selected_variant),
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
if config_path.exists() or config_path.is_symlink():
|
|
192
|
+
if config_path.is_symlink():
|
|
193
|
+
raise BundleError(f"bundle UtterVerify config must not be a symlink: {config_path}")
|
|
194
|
+
config_file = _contained_file(config_path, identity.root)
|
|
195
|
+
selected_config = load_config(config_file)
|
|
196
|
+
bundle_checks.append(DoctorCheck("bundle_config", "pass", str(config_file)))
|
|
197
|
+
result = run_doctor(
|
|
198
|
+
config_path=config_file,
|
|
199
|
+
config=selected_config,
|
|
200
|
+
backend=backend,
|
|
201
|
+
device=device,
|
|
202
|
+
model=model,
|
|
203
|
+
)
|
|
204
|
+
else:
|
|
205
|
+
selected_config = default_config()
|
|
206
|
+
bundle_checks.append(
|
|
207
|
+
DoctorCheck(
|
|
208
|
+
"bundle_config",
|
|
209
|
+
"warn",
|
|
210
|
+
"config is missing; doctor uses defaults and bundle verify would create it",
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
result = run_doctor(
|
|
214
|
+
config=selected_config,
|
|
215
|
+
backend=backend,
|
|
216
|
+
device=device,
|
|
217
|
+
model=model,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
units = _resolve_units(identity, selected_variant, "all")
|
|
222
|
+
for unit in units:
|
|
223
|
+
handoff_plan, handoff_manifest = _load_handoff(
|
|
224
|
+
unit.renderplan,
|
|
225
|
+
unit.render_dir,
|
|
226
|
+
plan_set=unit.plan_set,
|
|
227
|
+
set_entry=unit.set_entry,
|
|
228
|
+
)
|
|
229
|
+
_preflight_loaded(handoff_plan, handoff_manifest, unit.render_dir)
|
|
230
|
+
except UtterVerifyError:
|
|
231
|
+
raise
|
|
232
|
+
except Exception as exc:
|
|
233
|
+
raise BundleError(f"cannot preflight selected bundle inputs: {exc}") from exc
|
|
234
|
+
bundle_checks.append(
|
|
235
|
+
DoctorCheck("bundle_inputs", "pass", f"{len(units)} selected handoff(s) are complete")
|
|
236
|
+
)
|
|
237
|
+
checks = tuple(bundle_checks) + result.checks
|
|
238
|
+
return replace(
|
|
239
|
+
result,
|
|
240
|
+
ok=result.ok and all(check.status != "fail" for check in bundle_checks),
|
|
241
|
+
checks=checks,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _check_renderplan(checks: list[DoctorCheck]) -> None:
|
|
246
|
+
"""Check that installed RenderPlan exposes the handoff contracts we require."""
|
|
247
|
+
try:
|
|
248
|
+
module = importlib.import_module("renderplan")
|
|
249
|
+
except ImportError as exc:
|
|
250
|
+
checks.append(
|
|
251
|
+
DoctorCheck(
|
|
252
|
+
"renderplan",
|
|
253
|
+
"fail",
|
|
254
|
+
f"renderplan is not importable: {exc}",
|
|
255
|
+
"Install UtterVerify and its RenderPlan dependency.",
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
version = package_version("renderplan")
|
|
262
|
+
except PackageNotFoundError:
|
|
263
|
+
version = getattr(module, "__version__", "unknown version")
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
contracts = module.supported_contracts()
|
|
267
|
+
except Exception as exc: # noqa: BLE001
|
|
268
|
+
checks.append(
|
|
269
|
+
DoctorCheck(
|
|
270
|
+
"renderplan",
|
|
271
|
+
"fail",
|
|
272
|
+
f"cannot inspect RenderPlan public contracts: {exc}",
|
|
273
|
+
"Install renderplan>=0.2.1,<0.3.",
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
expected_apis = (
|
|
279
|
+
"load_manifest",
|
|
280
|
+
"load_plan",
|
|
281
|
+
"load_renderplan_set",
|
|
282
|
+
"validate_manifest_handoff",
|
|
283
|
+
"validate_renderplan_set",
|
|
284
|
+
"validate_set_chapter_plan",
|
|
285
|
+
)
|
|
286
|
+
missing_apis = [name for name in expected_apis if not callable(getattr(module, name, None))]
|
|
287
|
+
renderplan_contract = contracts.get("renderplan") if isinstance(contracts, Mapping) else None
|
|
288
|
+
if renderplan_contract != 2 or missing_apis:
|
|
289
|
+
problems = []
|
|
290
|
+
if renderplan_contract != 2:
|
|
291
|
+
problems.append(
|
|
292
|
+
f"RenderPlan contract v2 is unsupported (found {renderplan_contract!r})"
|
|
293
|
+
)
|
|
294
|
+
if missing_apis:
|
|
295
|
+
problems.append(f"missing public APIs: {', '.join(missing_apis)}")
|
|
296
|
+
checks.append(
|
|
297
|
+
DoctorCheck(
|
|
298
|
+
"renderplan",
|
|
299
|
+
"fail",
|
|
300
|
+
f"renderplan {version}: " + "; ".join(problems),
|
|
301
|
+
"Install renderplan>=0.2.1,<0.3.",
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
return
|
|
305
|
+
|
|
306
|
+
checks.append(
|
|
307
|
+
DoctorCheck(
|
|
308
|
+
"renderplan",
|
|
309
|
+
"pass",
|
|
310
|
+
f"renderplan {version} supports RenderPlan contract v2 and required public handoff APIs",
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _import_speechscope(checks: list[DoctorCheck]):
|
|
316
|
+
try:
|
|
317
|
+
module = importlib.import_module("speechscope")
|
|
318
|
+
except ImportError as exc:
|
|
319
|
+
checks.append(
|
|
320
|
+
DoctorCheck(
|
|
321
|
+
"speechscope",
|
|
322
|
+
"fail",
|
|
323
|
+
f"speechscope is not importable: {exc}",
|
|
324
|
+
"Install UtterVerify and its SpeechScope dependency.",
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
return None
|
|
328
|
+
version = getattr(module, "__version__", None)
|
|
329
|
+
if not isinstance(version, str):
|
|
330
|
+
try:
|
|
331
|
+
from importlib.metadata import version as package_version
|
|
332
|
+
|
|
333
|
+
version = package_version("speechscope")
|
|
334
|
+
except Exception: # noqa: BLE001
|
|
335
|
+
version = None
|
|
336
|
+
detail = f"speechscope {version}" if version else "speechscope is importable"
|
|
337
|
+
checks.append(DoctorCheck("speechscope", "pass", detail))
|
|
338
|
+
return module
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _check_redux(backend_info: object, checks: list[DoctorCheck]) -> None:
|
|
342
|
+
version = getattr(backend_info, "version", None)
|
|
343
|
+
if version is None:
|
|
344
|
+
checks.append(
|
|
345
|
+
DoctorCheck(
|
|
346
|
+
"redux_dependencies",
|
|
347
|
+
"fail",
|
|
348
|
+
"SpeechScope reports that the Redux dependency is unavailable",
|
|
349
|
+
"pip install 'utterverify[redux]'",
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
return
|
|
353
|
+
checks.append(DoctorCheck("redux_dependencies", "pass", f"dependency metadata: {version}"))
|
|
354
|
+
checks.append(
|
|
355
|
+
DoctorCheck(
|
|
356
|
+
"redux_runtime_readiness",
|
|
357
|
+
"warn",
|
|
358
|
+
"metadata preflight passed; full model/device readiness is not proven without runtime initialization",
|
|
359
|
+
)
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _check_whispercpp(
|
|
364
|
+
speechscope: object,
|
|
365
|
+
model: str | None,
|
|
366
|
+
model_source: ModelSource,
|
|
367
|
+
checks: list[DoctorCheck],
|
|
368
|
+
) -> None:
|
|
369
|
+
try:
|
|
370
|
+
executable, version = _probe_whisper_executable()
|
|
371
|
+
except Exception as exc: # noqa: BLE001
|
|
372
|
+
checks.append(
|
|
373
|
+
DoctorCheck(
|
|
374
|
+
"whisper_executable",
|
|
375
|
+
"fail",
|
|
376
|
+
str(exc),
|
|
377
|
+
"Install whisper.cpp or set SPEECHSCOPE_WHISPERCPP_BIN.",
|
|
378
|
+
)
|
|
379
|
+
)
|
|
380
|
+
else:
|
|
381
|
+
detail = executable + (f" (whisper.cpp {version})" if version else "")
|
|
382
|
+
checks.append(DoctorCheck("whisper_executable", "pass", detail))
|
|
383
|
+
|
|
384
|
+
if model is None or not model.strip():
|
|
385
|
+
checks.append(
|
|
386
|
+
DoctorCheck(
|
|
387
|
+
"model_selected",
|
|
388
|
+
"fail",
|
|
389
|
+
"No whisper.cpp model is selected.",
|
|
390
|
+
"Configure a model with `utterverify bundle configure <bundle> --engine whispercpp --model whispercpp:<id>` or set SPEECHSCOPE_WHISPERCPP_MODEL.",
|
|
391
|
+
)
|
|
392
|
+
)
|
|
393
|
+
return
|
|
394
|
+
checks.append(DoctorCheck("model_selected", "pass", model))
|
|
395
|
+
if model_source == "environment":
|
|
396
|
+
checks.append(
|
|
397
|
+
DoctorCheck(
|
|
398
|
+
"model_pinned",
|
|
399
|
+
"warn",
|
|
400
|
+
"using SPEECHSCOPE_WHISPERCPP_MODEL; bundle config does not pin a target model",
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
if model.startswith("whispercpp:"):
|
|
405
|
+
_check_managed_model(speechscope, model, checks)
|
|
406
|
+
else:
|
|
407
|
+
_check_local_model(model, checks)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _probe_whisper_executable() -> tuple[str, str | None]:
|
|
411
|
+
# Adapter-internal SpeechScope helper; isolated here in case a public API appears.
|
|
412
|
+
from speechscope.backends.whispercpp import probe_executable
|
|
413
|
+
|
|
414
|
+
return probe_executable()
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _check_managed_model(speechscope: object, ref: str, checks: list[DoctorCheck]) -> None:
|
|
418
|
+
manager_type = getattr(speechscope, "ModelManager", None)
|
|
419
|
+
if manager_type is None:
|
|
420
|
+
checks.append(
|
|
421
|
+
DoctorCheck("model_catalogued", "fail", "SpeechScope ModelManager is unavailable")
|
|
422
|
+
)
|
|
423
|
+
checks.extend(
|
|
424
|
+
DoctorCheck(name, "warn", "not checked")
|
|
425
|
+
for name in ("model_managed", "model_installed", "model_integrity", "model_readable")
|
|
426
|
+
)
|
|
427
|
+
return
|
|
428
|
+
try:
|
|
429
|
+
manager = manager_type(packaged_catalog=True)
|
|
430
|
+
metadata = manager.show(ref, backend="whispercpp")
|
|
431
|
+
except Exception as exc: # noqa: BLE001
|
|
432
|
+
checks.append(DoctorCheck("model_catalogued", "fail", str(exc)))
|
|
433
|
+
checks.extend(
|
|
434
|
+
DoctorCheck(name, "warn", "not checked because model is not catalogued")
|
|
435
|
+
for name in ("model_managed", "model_installed", "model_integrity", "model_readable")
|
|
436
|
+
)
|
|
437
|
+
return
|
|
438
|
+
checks.append(DoctorCheck("model_catalogued", "pass", str(metadata.get("ref", ref))))
|
|
439
|
+
|
|
440
|
+
acquisition = metadata.get("acquisition")
|
|
441
|
+
managed = isinstance(acquisition, dict) and acquisition.get("mode") == "managed"
|
|
442
|
+
if not managed:
|
|
443
|
+
checks.append(DoctorCheck("model_managed", "fail", f"{ref} is not SpeechScope-managed"))
|
|
444
|
+
checks.extend(
|
|
445
|
+
DoctorCheck(name, "warn", "not checked for an unmanaged catalog model")
|
|
446
|
+
for name in ("model_installed", "model_integrity", "model_readable")
|
|
447
|
+
)
|
|
448
|
+
return
|
|
449
|
+
checks.append(DoctorCheck("model_managed", "pass", "managed by SpeechScope"))
|
|
450
|
+
|
|
451
|
+
try:
|
|
452
|
+
path = Path(manager.path(ref, backend="whispercpp"))
|
|
453
|
+
except Exception as exc: # noqa: BLE001
|
|
454
|
+
error_name = type(exc).__name__
|
|
455
|
+
if error_name == "ModelNotInstalledError":
|
|
456
|
+
checks.append(
|
|
457
|
+
DoctorCheck(
|
|
458
|
+
"model_installed", "fail", str(exc), f"speechscope models install {ref}"
|
|
459
|
+
)
|
|
460
|
+
)
|
|
461
|
+
checks.extend(
|
|
462
|
+
DoctorCheck(name, "warn", "not checked because model is not installed")
|
|
463
|
+
for name in ("model_integrity", "model_readable")
|
|
464
|
+
)
|
|
465
|
+
elif error_name == "ModelIntegrityError":
|
|
466
|
+
checks.append(DoctorCheck("model_installed", "pass", "managed install found"))
|
|
467
|
+
checks.append(DoctorCheck("model_integrity", "fail", str(exc)))
|
|
468
|
+
checks.append(DoctorCheck("model_readable", "fail", "integrity verification failed"))
|
|
469
|
+
else:
|
|
470
|
+
checks.append(
|
|
471
|
+
DoctorCheck(
|
|
472
|
+
"model_installed", "fail", str(exc), f"speechscope models install {ref}"
|
|
473
|
+
)
|
|
474
|
+
)
|
|
475
|
+
checks.extend(
|
|
476
|
+
DoctorCheck(name, "warn", "not checked because managed model resolution failed")
|
|
477
|
+
for name in ("model_integrity", "model_readable")
|
|
478
|
+
)
|
|
479
|
+
return
|
|
480
|
+
|
|
481
|
+
checks.append(DoctorCheck("model_installed", "pass", str(path)))
|
|
482
|
+
checks.append(
|
|
483
|
+
DoctorCheck("model_integrity", "pass", "verified by SpeechScope ModelManager.path()")
|
|
484
|
+
)
|
|
485
|
+
if path.is_file() and os.access(path, os.R_OK):
|
|
486
|
+
checks.append(DoctorCheck("model_readable", "pass", str(path)))
|
|
487
|
+
else:
|
|
488
|
+
checks.append(
|
|
489
|
+
DoctorCheck("model_readable", "fail", f"model is not a readable regular file: {path}")
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _check_local_model(value: str, checks: list[DoctorCheck]) -> None:
|
|
494
|
+
path = Path(value).expanduser()
|
|
495
|
+
try:
|
|
496
|
+
if not path.is_file():
|
|
497
|
+
raise OSError("path is not an existing regular file")
|
|
498
|
+
with path.open("rb") as model_file:
|
|
499
|
+
model_file.read(1)
|
|
500
|
+
except OSError as exc:
|
|
501
|
+
checks.append(
|
|
502
|
+
DoctorCheck(
|
|
503
|
+
"model_readable",
|
|
504
|
+
"fail",
|
|
505
|
+
f"whisper.cpp model is not a readable local file: {path} ({exc})",
|
|
506
|
+
)
|
|
507
|
+
)
|
|
508
|
+
else:
|
|
509
|
+
checks.append(DoctorCheck("model_readable", "pass", str(path.resolve())))
|
utterverify/errors.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Public errors for UtterVerify."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class UtterVerifyError(Exception):
|
|
5
|
+
"""Base error."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConfigError(UtterVerifyError, ValueError):
|
|
9
|
+
"""Configuration is invalid."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BundleError(UtterVerifyError, ValueError):
|
|
13
|
+
"""Bundle input or layout is invalid."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HandoffError(UtterVerifyError, ValueError):
|
|
17
|
+
"""RenderPlan / VoiceRender handoff is incomplete or inconsistent."""
|
utterverify/py.typed
ADDED
|
File without changes
|
utterverify/types.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Result models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
SegmentStatus = Literal["pass", "review", "fail", "not_evaluated", "error"]
|
|
10
|
+
UnitStatus = Literal["pass", "review", "fail", "error"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class SegmentOutcome:
|
|
15
|
+
number: int
|
|
16
|
+
segment_id: str
|
|
17
|
+
reference_text: str
|
|
18
|
+
wav: Path
|
|
19
|
+
wav_sha256: str
|
|
20
|
+
report: Path | None
|
|
21
|
+
status: SegmentStatus
|
|
22
|
+
language: str | None = None
|
|
23
|
+
message: str | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True)
|
|
27
|
+
class UnitVerifyResult:
|
|
28
|
+
id: str
|
|
29
|
+
renderplan: Path
|
|
30
|
+
render_plan_id: str
|
|
31
|
+
source_plan_id: str
|
|
32
|
+
render_profile_id: str
|
|
33
|
+
render_dir: Path
|
|
34
|
+
manifest_id: str | None
|
|
35
|
+
output_dir: Path
|
|
36
|
+
segments: tuple[SegmentOutcome, ...]
|
|
37
|
+
status: UnitStatus
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def passed(self) -> int:
|
|
41
|
+
return sum(item.status == "pass" for item in self.segments)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def reviewed(self) -> int:
|
|
45
|
+
return sum(item.status in {"review", "not_evaluated"} for item in self.segments)
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def failed(self) -> int:
|
|
49
|
+
return sum(item.status == "fail" for item in self.segments)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def errors(self) -> int:
|
|
53
|
+
return sum(item.status == "error" for item in self.segments)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True)
|
|
57
|
+
class BundleVerifyResult:
|
|
58
|
+
bundle: Path
|
|
59
|
+
kind: Literal["book", "document"]
|
|
60
|
+
variant: str
|
|
61
|
+
output_dir: Path
|
|
62
|
+
config_path: Path
|
|
63
|
+
report_path: Path
|
|
64
|
+
config_created: bool
|
|
65
|
+
units: tuple[UnitVerifyResult, ...]
|
|
66
|
+
status: UnitStatus
|
|
67
|
+
fail_on_review: bool = False
|
|
68
|
+
|
|
69
|
+
speechscope_backend: str = "redux"
|
|
70
|
+
speechscope_device: str = "cpu"
|
|
71
|
+
speechscope_model: str | None = None
|
|
72
|
+
normalization: str = "basic-v1"
|
|
73
|
+
use_language_hint: bool = False
|
|
74
|
+
config_sha256: str | None = None
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def segments(self) -> int:
|
|
78
|
+
return sum(len(unit.segments) for unit in self.units)
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def passed(self) -> int:
|
|
82
|
+
return sum(unit.passed for unit in self.units)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def reviewed(self) -> int:
|
|
86
|
+
return sum(unit.reviewed for unit in self.units)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def failed(self) -> int:
|
|
90
|
+
return sum(unit.failed for unit in self.units)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def errors(self) -> int:
|
|
94
|
+
return sum(unit.errors for unit in self.units)
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def exit_code(self) -> int:
|
|
98
|
+
if self.status == "error":
|
|
99
|
+
return 2
|
|
100
|
+
if self.status == "fail" or (self.status == "review" and self.fail_on_review):
|
|
101
|
+
return 1
|
|
102
|
+
return 0
|