Retain bounded invalid ASR output and atomic failure evidence
This commit is contained in:
1 parent
c33047998b
commit
b002399217
3 files changed
+99
-3
No files matched your search
@@ -115,5 +115,15 @@ SHA at completion while the on-disk harness had changed; that field cannot prove
|
|||||||
the exact executing harness bytes. Its original raw token failures remain valid
|
the exact executing harness bytes. Its original raw token failures remain valid
|
||||||
diagnostic observations, and its excluded window already prohibits an accepted
|
diagnostic observations, and its excluded window already prohibits an accepted
|
||||||
quality score. Preserve that historical limitation instead of rewriting output.
|
quality score. Preserve that historical limitation instead of rewriting output.
|
||||||
|
The first DTW run stopped on strict UTF-8 parsing in the second piano window
|
||||||
|
(offset 30000 ms): native JSON bytes at 1137–1138 were invalid. Its temporary
|
||||||
|
raw output was not retained by that harness, so exact malformed bytes/native
|
||||||
|
cause cannot be established. The partial checkpoint retains all three NASA
|
||||||
|
windows (21 accepted DTW sentence spans, including one independently recorded
|
||||||
|
ordinary-interval failure) and the first piano window. Missing remaining windows
|
||||||
|
and sources prohibit any accepted retrieval score. It was not rerun or decoded
|
||||||
|
lossily. Subsequent harnesses retain bounded exact failing output bytes under
|
||||||
|
the owned scratch parent with SHA/size/parse position, and atomically record a
|
||||||
|
failed incomplete checkpoint before terminating; they do not skip competitors.
|
||||||
References: pinned [token data/API](https://github.com/ggml-org/whisper.cpp/blob/d1be6fde11ac6e0407606b4e42fe72d34add8037/include/whisper.h)
|
References: pinned [token data/API](https://github.com/ggml-org/whisper.cpp/blob/d1be6fde11ac6e0407606b4e42fe72d34add8037/include/whisper.h)
|
||||||
and [DTW implementation](https://github.com/ggml-org/whisper.cpp/blob/d1be6fde11ac6e0407606b4e42fe72d34add8037/src/whisper.cpp).
|
and [DTW implementation](https://github.com/ggml-org/whisper.cpp/blob/d1be6fde11ac6e0407606b4e42fe72d34add8037/src/whisper.cpp).
|
||||||
@@ -31,6 +31,50 @@ def sha256(path):
|
|||||||
return digest.hexdigest()
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
class AsrOutputError(ValueError):
|
||||||
|
"""Bounded raw output retained; callers must keep coverage incomplete."""
|
||||||
|
def __init__(self, failure):
|
||||||
|
super().__init__("ASR output failed strict UTF-8/JSON parsing; raw evidence retained")
|
||||||
|
self.failure = failure
|
||||||
|
|
||||||
|
|
||||||
|
def checkpoint_report(path, document):
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
temporary_path = None
|
||||||
|
try:
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=path.parent,
|
||||||
|
prefix="speech-checkpoint-", suffix=".tmp", delete=False) as temporary:
|
||||||
|
temporary_path = Path(temporary.name)
|
||||||
|
json.dump(document, temporary, indent=2, ensure_ascii=False)
|
||||||
|
temporary.write("\n")
|
||||||
|
temporary.flush()
|
||||||
|
os.fsync(temporary.fileno())
|
||||||
|
os.replace(temporary_path, path)
|
||||||
|
finally:
|
||||||
|
if temporary_path is not None:
|
||||||
|
temporary_path.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_asr_output(path, retained_parent):
|
||||||
|
with path.open("rb") as stream:
|
||||||
|
raw = stream.read(MAX_OUTPUT + 1)
|
||||||
|
if len(raw) > MAX_OUTPUT:
|
||||||
|
raise ValueError("ASR output exceeds parse budget")
|
||||||
|
try:
|
||||||
|
return json.loads(raw.decode("utf-8"))
|
||||||
|
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
||||||
|
with tempfile.NamedTemporaryFile(prefix="speech-asr-output-failure-", suffix=".bin",
|
||||||
|
dir=retained_parent, delete=False) as retained:
|
||||||
|
retained.write(raw)
|
||||||
|
failure = {"error_type": type(error).__name__, "raw_output_path": retained.name,
|
||||||
|
"raw_output_bytes": len(raw), "raw_output_sha256": hashlib.sha256(raw).hexdigest()}
|
||||||
|
if isinstance(error, UnicodeDecodeError):
|
||||||
|
failure.update(byte_start=error.start, byte_end=error.end, reason=error.reason)
|
||||||
|
else:
|
||||||
|
failure.update(character_position=error.pos, line=error.lineno, column=error.colno)
|
||||||
|
raise AsrOutputError(failure) from error
|
||||||
|
|
||||||
|
|
||||||
def stop(child):
|
def stop(child):
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
subprocess.run(["taskkill", "/PID", str(child.pid), "/T", "/F"],
|
subprocess.run(["taskkill", "/PID", str(child.pid), "/T", "/F"],
|
||||||
@@ -228,7 +272,25 @@ def main():
|
|||||||
"--output-json-full" if alignment_mode else "--output-json",
|
"--output-json-full" if alignment_mode else "--output-json",
|
||||||
*(["--dtw", "base", "--no-flash-attn"] if args.dtw_sentences else []),
|
*(["--dtw", "base", "--no-flash-attn"] if args.dtw_sentences else []),
|
||||||
"--output-file", str(prefix)], scratch, "asr", [(output, MAX_OUTPUT)], 600)
|
"--output-file", str(prefix)], scratch, "asr", [(output, MAX_OUTPUT)], 600)
|
||||||
raw = json.loads(output.read_text(encoding="utf-8"))
|
try:
|
||||||
|
raw = parse_asr_output(output, args.scratch_parent)
|
||||||
|
except AsrOutputError as error:
|
||||||
|
result["windows"].append({"offset_ms": offset, "duration_ms": length,
|
||||||
|
"decode_ms": decode_ms, "asr_process_ms": asr_ms,
|
||||||
|
"decoded_frames": frames, "segments": [],
|
||||||
|
"error_stage": "asr_output_parse", "validation_error": str(error),
|
||||||
|
"actual_output_failure": error.failure})
|
||||||
|
failed = {"complete": False, "quality_failed": True,
|
||||||
|
"comparison_eligible": False, "fixture_sha256": sha256(args.fixture),
|
||||||
|
"model_sha256": digest, "worker_sha256": args.whisper_sha256,
|
||||||
|
"harness_sha256": HARNESS_SHA256_AT_START, "rows": [*rows, result],
|
||||||
|
"failure": {"asset": asset["id"], "offset_ms": offset, **error.failure}}
|
||||||
|
if alignment_mode:
|
||||||
|
failed.update(alignment_policy=policy["policy"],
|
||||||
|
alignment_policy_sha256=alignment_policy_sha,
|
||||||
|
alignment_contract_sha256=alignment_contract_sha)
|
||||||
|
checkpoint_report(args.output, failed)
|
||||||
|
raise
|
||||||
result["windows"].append(recognized_window(raw, offset, length, decode_ms, asr_ms))
|
result["windows"].append(recognized_window(raw, offset, length, decode_ms, asr_ms))
|
||||||
result["windows"][-1]["decoded_frames"] = frames
|
result["windows"][-1]["decoded_frames"] = frames
|
||||||
if alignment_mode:
|
if alignment_mode:
|
||||||
@@ -250,7 +312,7 @@ def main():
|
|||||||
checkpoint["alignment_policy"] = policy["policy"]
|
checkpoint["alignment_policy"] = policy["policy"]
|
||||||
checkpoint["alignment_policy_sha256"] = alignment_policy_sha
|
checkpoint["alignment_policy_sha256"] = alignment_policy_sha
|
||||||
checkpoint["alignment_contract_sha256"] = alignment_contract_sha
|
checkpoint["alignment_contract_sha256"] = alignment_contract_sha
|
||||||
args.output.write_text(json.dumps(checkpoint, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
checkpoint_report(args.output, checkpoint)
|
||||||
if source.stat().st_size != asset["bytes"] or sha256(source) != asset["sha256"]:
|
if source.stat().st_size != asset["bytes"] or sha256(source) != asset["sha256"]:
|
||||||
raise ValueError("Source changed during recognition")
|
raise ValueError("Source changed during recognition")
|
||||||
rows.append(result)
|
rows.append(result)
|
||||||
@@ -275,7 +337,7 @@ def main():
|
|||||||
report["alignment_contract_sha256"] = alignment_contract_sha
|
report["alignment_contract_sha256"] = alignment_contract_sha
|
||||||
report["alignment_policy_sha256"] = alignment_policy_sha
|
report["alignment_policy_sha256"] = alignment_policy_sha
|
||||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
args.output.write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
checkpoint_report(args.output, report)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -10,9 +10,33 @@ import importlib.util
|
|||||||
import hashlib
|
import hashlib
|
||||||
from run import segments, run_owned, recognized_window, decode_command, decoded_frames, primary_audio_range
|
from run import segments, run_owned, recognized_window, decode_command, decoded_frames, primary_audio_range
|
||||||
from score import score, require_eligible
|
from score import score, require_eligible
|
||||||
|
from run import parse_asr_output, AsrOutputError, MAX_OUTPUT, checkpoint_report
|
||||||
|
|
||||||
|
|
||||||
class ContractTests(unittest.TestCase):
|
class ContractTests(unittest.TestCase):
|
||||||
|
def test_atomic_checkpoint_failure_preserves_previous_evidence(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
path = Path(directory) / "report.json"
|
||||||
|
checkpoint_report(path, {"complete": False, "rows": []})
|
||||||
|
old = path.read_bytes()
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
checkpoint_report(path, {"invalid": object()})
|
||||||
|
self.assertEqual(path.read_bytes(), old)
|
||||||
|
def test_invalid_asr_utf8_and_json_retain_exact_bounded_bytes(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
parent = Path(directory)
|
||||||
|
path = parent / "output.json"
|
||||||
|
for raw in [b'{"text":"\xe2\x28\xa1"}', b'{"text":']:
|
||||||
|
path.write_bytes(raw)
|
||||||
|
with self.assertRaises(AsrOutputError) as caught:
|
||||||
|
parse_asr_output(path, parent)
|
||||||
|
failure = caught.exception.failure
|
||||||
|
self.assertEqual(Path(failure["raw_output_path"]).read_bytes(), raw)
|
||||||
|
self.assertEqual(failure["raw_output_sha256"], hashlib.sha256(raw).hexdigest())
|
||||||
|
self.assertEqual(failure["raw_output_bytes"], len(raw))
|
||||||
|
path.write_bytes(b"x" * (MAX_OUTPUT + 1))
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
parse_asr_output(path, parent)
|
||||||
def test_harness_source_hash_is_captured_before_file_changes(self):
|
def test_harness_source_hash_is_captured_before_file_changes(self):
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
path = Path(directory) / "run-copy.py"
|
path = Path(directory) / "run-copy.py"
|
||||||
|
|||||||
Reference in new issue
Block a user