Codex Release Audit commited on
Commit
b0a09b6
·
1 Parent(s): f980128

Harden network endpoint and local verification

Browse files
README.md CHANGED
@@ -56,10 +56,10 @@ Barbet 另固定在 `6fcd7ce4aa37f2250a3242995bef0fbc3b026ba8`,
56
  | Email / URL frontend | hybrid lexical labels; scheme, all-uppercase atoms and short suffixes use explicit ASCII letter tokens; digits are read one by one and separators remain audible |
57
  | Stop policy | 0.50 → 0.05 from 75% to 95% predicted progress, 1 hit |
58
  | Endpoint cue | append terminal punctuation for model input when missing, except very short text |
59
- | Hard stop | native-pace target steps, independent of playback pace |
60
  | Pace correction | derive total/active pace from the completed raw waveform, then one pitch-preserving STFT stretch (ordinary `1536/384`; URL/email code-switch `2048/512`); no active-interval slicing |
61
  | Pace / speaker eligibility duration | union of active 25 ms RMS frames at 10 ms hop; internal pauses excluded |
62
- | Semantic verification | orthographic whole CER + tone-aware acoustic first/last 6 exact + no lexical tail; turbo/full-large-v3 hard intersection; fail closed |
63
  | Local endpoint verification | internal chunk first/last 6 may contain at most one substitution and zero deletion; absolute request onset/ending remain exact |
64
  | Acoustic quality gate | pinned CPU SQUIM objective SHA-256 `2c54586fea83fb5eb5394d710038ee89f55cab7011a5bf730bebed4c8777e828`; hard STOI ≥ 0.60 / PESQ ≥ 1.12; at ≥1.50 s, single-chunk early return additionally requires preferred STOI ≥ 0.72 / PESQ ≥ 1.20 and speaker 0.25 / 0.05; SI-SDR is bounded soft evidence only |
65
  | Local speaker verification | frozen request centroid + bounded window begin/end directional drift |
@@ -97,10 +97,16 @@ margin 固定 +1 latent step、`min_len` 固定為 2,不會為了播放目標
97
  正奇數使用 alternate CFG 2.0;含 URL/email component 的 chunk 仍有 CFG 3.0 的內容安全下限。
98
  同一列 log 也記錄實際 generated chunks/text units 與各 row 的有效候選數。
99
  每次生成前另外記錄 seed、local chunk index、duration policy、scheduled CFG,
100
- 以及 network/short-text floor 後的 effective CFG;因此即使最後 fail closed 也能重建嘗試軌跡。
 
 
 
 
 
101
  終端 outcome 另輸出 content-free canonical evidence schema v4,包含 original chunk indices、
102
  row-local ordinal、network provenance、CFG contract、32/800 預算用量與 selected-path 交叉檢查,
103
- 以及 local、joined、independent-large-v3 與 final gate 的 bounded SQUIM scalar,
 
104
  不包含 target/transcript/audio/embedding。
105
  為了讓 release contract 與正式量測一致,UI 不提供其他 primary CFG。
106
  內部 hosted evaluator 可注入 `[0, 2^31)` 的固定 root seed 以重現結果;UI 不暴露這個參數,
@@ -114,9 +120,13 @@ Ragged DP 可混合已通過 hard gate 的 chunks,
114
  完成 RMS matching、edge fade、pause、crossfade 與使用者 speed 後,服務會再對最終整段 waveform
115
  執行 normalized target 的 prefix/whole/suffix/tail、pace 與 speaker anchor/boundary gate,並要求
116
  turbo 與 full large-v3 的語意 hard intersection;這個 post-join gate 失敗時不會回傳先前已通過的
117
- chunk 音訊。Full large-v3 不驗 local chunk,只驗 production assembler 產生的 exact whole candidate
118
- 或 exact sequence path;initial whole rejection 會啟動低覆蓋 refill,sequence rejection 則只會
119
- 前進到下一條已排序的 exact path。
 
 
 
 
120
  此外,初始 same-seed trajectory 只有在每個 local chunk 都通過後,才會先用同一個
121
  `_assemble_trajectory_audio` 組成實際播放版本並跑整段 semantic/pace/speaker gate;joined gate
122
  失敗會取消該 whole trajectory 的資格,但保留已通過的 local evidence 供 sequence DP 使用。
 
56
  | Email / URL frontend | hybrid lexical labels; scheme, all-uppercase atoms and short suffixes use explicit ASCII letter tokens; digits are read one by one and separators remain audible |
57
  | Stop policy | 0.50 → 0.05 from 75% to 95% predicted progress, 1 hit |
58
  | Endpoint cue | append terminal punctuation for model input when missing, except very short text |
59
+ | Hard stop | native-pace target steps, independent of playback pace; URL/email chunks use a separate conservative endpoint-duration counter (ASCII alnum run ÷ 2) |
60
  | Pace correction | derive total/active pace from the completed raw waveform, then one pitch-preserving STFT stretch (ordinary `1536/384`; URL/email code-switch `2048/512`); no active-interval slicing |
61
  | Pace / speaker eligibility duration | union of active 25 ms RMS frames at 10 ms hop; internal pauses excluded |
62
+ | Semantic verification | orthographic whole CER + tone-aware acoustic first/last 6 exact + no lexical tail; range-proven URL/email locals and every exact whole output require turbo/full-large-v3 hard intersection; fail closed |
63
  | Local endpoint verification | internal chunk first/last 6 may contain at most one substitution and zero deletion; absolute request onset/ending remain exact |
64
  | Acoustic quality gate | pinned CPU SQUIM objective SHA-256 `2c54586fea83fb5eb5394d710038ee89f55cab7011a5bf730bebed4c8777e828`; hard STOI ≥ 0.60 / PESQ ≥ 1.12; at ≥1.50 s, single-chunk early return additionally requires preferred STOI ≥ 0.72 / PESQ ≥ 1.20 and speaker 0.25 / 0.05; SI-SDR is bounded soft evidence only |
65
  | Local speaker verification | frozen request centroid + bounded window begin/end directional drift |
 
97
  正奇數使用 alternate CFG 2.0;含 URL/email component 的 chunk 仍有 CFG 3.0 的內容安全下限。
98
  同一列 log 也記錄實際 generated chunks/text units 與各 row 的有效候選數。
99
  每次生成前另外記錄 seed、local chunk index、duration policy、scheduled CFG,
100
+ endpoint-duration units/counter,以及 network/short-text floor 後的 effective CFG;因此即使最後
101
+ fail closed 也能重建嘗試軌跡。公開 pace、chunk planner 與 32/800 work budget 仍使用原本的
102
+ speech-unit counter;只有 URL/email-conditioned native endpoint estimate 對連續 ASCII alnum run
103
+ 採用除以 2 的保守 counter。這個 counter 刻意同時決定 adaptive weak-stop 的
104
+ `expected_steps` progress 與 `max_len` hard cap;兩者屬於同一份 native endpoint plan,
105
+ 但不會進入播放 pace、chunk planning 或 32/800 work accounting,`min_len` 仍固定為 2。
106
  終端 outcome 另輸出 content-free canonical evidence schema v4,包含 original chunk indices、
107
  row-local ordinal、network provenance、CFG contract、32/800 預算用量與 selected-path 交叉檢查,
108
+ 每個 local row 的 independent-large-v3 attempted/pass/proof-count/result attestation,以及
109
+ local、joined、independent-large-v3 與 final gate 的 bounded SQUIM scalar,
110
  不包含 target/transcript/audio/embedding。
111
  為了讓 release contract 與正式量測一致,UI 不提供其他 primary CFG。
112
  內部 hosted evaluator 可注入 `[0, 2^31)` 的固定 root seed 以重現結果;UI 不暴露這個參數,
 
120
  完成 RMS matching、edge fade、pause、crossfade 與使用者 speed 後,服務會再對最終整段 waveform
121
  執行 normalized target 的 prefix/whole/suffix/tail、pace 與 speaker anchor/boundary gate,並要求
122
  turbo 與 full large-v3 的語意 hard intersection;這個 post-join gate 失敗時不會回傳先前已通過的
123
+ chunk 音訊。除此之外,每個帶有 planner range proof 的 URL/email local chunk 在進入 coverage pool
124
+ 以前,也必須用完全相同的 `NetworkFragmentProof` 同時通過 turbo 與 pinned full large-v3;
125
+ large-v3 reject 會成為不可被 boundary proxy 放寬的 semantic hard failure。一般 local chunk
126
+ 維持 turbo gate;已被 turbo 的非 boundary hard gate 拒絕、原本就不可能進 pool 的 network row
127
+ 不額外耗用 full large-v3。Production assembler 產生的 exact whole candidate 或 exact sequence path 仍全部
128
+ 使用 dual-ASR。Initial whole rejection 會啟動低覆蓋 refill,sequence rejection 則只會前進到
129
+ 下一條已排序的 exact path。
130
  此外,初始 same-seed trajectory 只有在每個 local chunk 都通過後,才會先用同一個
131
  `_assemble_trajectory_audio` 組成實際播放版本並跑整段 semantic/pace/speaker gate;joined gate
132
  失敗會取消該 whole trajectory 的資格,但保留已通過的 local evidence 供 sequence DP 使用。
app.py CHANGED
@@ -24,6 +24,7 @@ from production import (
24
  apply_loudness_floor,
25
  canonicalize_asr_network_fragments,
26
  coalesce_text_chunks,
 
27
  contains_network_identifier,
28
  count_speech_units,
29
  effective_generation_cfg,
@@ -59,16 +60,20 @@ from quality_runtime import (
59
  ChunkCandidateArtifact,
60
  FinalOutputRejectedError,
61
  GenerationPolicy,
 
62
  NoQualifiedCandidateError,
63
  RELEASE_SPEAKER_TRIGGER_SECONDS,
64
  SEQUENCE_FALLBACK_MAX_LOCAL_BOUNDARY_SPEAKER_DROP,
65
  WholeWaveformVerificationCache,
66
  active_voiced_duration_seconds,
 
67
  active_audio_rms_db,
68
  active_audio_median_f0_hz,
69
  format_cascade_evidence_log,
70
  generation_cfg_for_candidate_offset,
71
  generation_policy_for_candidate_offset,
 
 
72
  prepare_candidate_audio,
73
  qualify_trajectory_with_joined_output,
74
  require_verified_final_output,
@@ -313,12 +318,18 @@ def _generate_chunk(
313
  ascii_cps=policy.ascii_cps,
314
  )
315
  )
 
 
 
 
 
316
  model_text, expected_steps, hard_stop_steps = endpoint_generation_plan(
317
  text,
318
  generation_cps=generation_cps,
319
  step_seconds=STEP_SECONDS,
320
  margin_steps=policy.hard_stop_margin_steps,
321
  add_terminal_punctuation=count_speech_units(text) >= MIN_ENDPOINT_CUE_UNITS,
 
322
  )
323
  # Do not hold generation open to enforce pace. The model can finish the
324
  # requested text early; extending its latent sequence creates tail speech.
@@ -368,7 +379,10 @@ def _generate_chunk(
368
  print(
369
  "[BlueMagpie] generation policy "
370
  f"name={policy.name} seed={request_seed} generation_cps={generation_cps:.2f} "
371
- f"expected_steps={expected_steps} hard_stop_steps={hard_stop_steps} min_len={min_len}"
 
 
 
372
  )
373
  audio = audio.detach().float().cpu().numpy().reshape(-1)
374
  pace_speed = target_pace_speed(
@@ -886,6 +900,101 @@ def _verification_metric_log_fields(verification) -> str:
886
  )
887
 
888
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
889
  def _qualify_candidate_trajectory_audio(
890
  trajectory: tuple[np.ndarray, ...],
891
  chunks: tuple[str, ...],
@@ -899,16 +1008,30 @@ def _qualify_candidate_trajectory_audio(
899
  ):
900
  """Run whole-output qualification only after every local chunk passes."""
901
 
 
902
  local_verification = _verify_trajectory_audio(
903
  trajectory,
904
  chunks,
905
  anchor,
906
  playback_speed,
907
- network_fragment_proofs=_network_fragment_proof_rows(chunks, chunk_specs),
908
  local_endpoint_roles=_local_endpoint_role_rows(chunks, chunk_specs),
909
  )
 
 
 
 
 
 
 
 
 
 
910
  if not local_verification.passed:
911
- return CandidateVerification(local_verification)
 
 
 
912
 
913
  waveform = _assemble_trajectory_audio(
914
  trajectory,
@@ -937,6 +1060,7 @@ def _qualify_candidate_trajectory_audio(
937
  )
938
  return CandidateVerification(
939
  qualified,
 
940
  joined_output=joined_evidence,
941
  )
942
 
@@ -958,6 +1082,7 @@ def _qualify_candidate_trajectory_audio(
958
  )
959
  return CandidateVerification(
960
  dual_qualified,
 
961
  joined_output=joined_evidence,
962
  independent_output=trajectory_gate_evidence(independent_verification),
963
  )
@@ -969,19 +1094,36 @@ def _verify_refill_candidate_trajectory_audio(
969
  anchor: np.ndarray,
970
  playback_speed: float,
971
  chunk_specs: tuple[GenerationChunkSpec, ...] | None = None,
 
 
972
  ):
973
- """Apply the unchanged strict local gates to one safe-duration refill."""
974
 
975
  if len(trajectory) != 1 or len(chunks) != 1:
976
  return verify_trajectory(())
977
- return _verify_trajectory_audio(
 
978
  trajectory,
979
  chunks,
980
  anchor,
981
  playback_speed,
982
- network_fragment_proofs=_network_fragment_proof_rows(chunks, chunk_specs),
983
  local_endpoint_roles=_local_endpoint_role_rows(chunks, chunk_specs),
984
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
985
 
986
 
987
  def _verify_sequence_trajectory_audio(
@@ -1331,6 +1473,7 @@ def _synthesize(
1331
  anchor,
1332
  speed,
1333
  generation_chunk_specs(seed, candidate_chunks),
 
1334
  )
1335
  ),
1336
  sequence_final_verifier=lambda sequence_result, candidate_chunks: (
 
24
  apply_loudness_floor,
25
  canonicalize_asr_network_fragments,
26
  coalesce_text_chunks,
27
+ count_network_endpoint_duration_units,
28
  contains_network_identifier,
29
  count_speech_units,
30
  effective_generation_cfg,
 
60
  ChunkCandidateArtifact,
61
  FinalOutputRejectedError,
62
  GenerationPolicy,
63
+ LocalIndependentGateEvidence,
64
  NoQualifiedCandidateError,
65
  RELEASE_SPEAKER_TRIGGER_SECONDS,
66
  SEQUENCE_FALLBACK_MAX_LOCAL_BOUNDARY_SPEAKER_DROP,
67
  WholeWaveformVerificationCache,
68
  active_voiced_duration_seconds,
69
+ candidate_gate_evidence,
70
  active_audio_rms_db,
71
  active_audio_median_f0_hz,
72
  format_cascade_evidence_log,
73
  generation_cfg_for_candidate_offset,
74
  generation_policy_for_candidate_offset,
75
+ intersect_local_semantic_verification,
76
+ local_candidate_has_coverage_eligibility,
77
  prepare_candidate_audio,
78
  qualify_trajectory_with_joined_output,
79
  require_verified_final_output,
 
318
  ascii_cps=policy.ascii_cps,
319
  )
320
  )
321
+ endpoint_duration_units = (
322
+ count_network_endpoint_duration_units(text)
323
+ if network_conditioned
324
+ else count_speech_units(text)
325
+ )
326
  model_text, expected_steps, hard_stop_steps = endpoint_generation_plan(
327
  text,
328
  generation_cps=generation_cps,
329
  step_seconds=STEP_SECONDS,
330
  margin_steps=policy.hard_stop_margin_steps,
331
  add_terminal_punctuation=count_speech_units(text) >= MIN_ENDPOINT_CUE_UNITS,
332
+ duration_units=endpoint_duration_units,
333
  )
334
  # Do not hold generation open to enforce pace. The model can finish the
335
  # requested text early; extending its latent sequence creates tail speech.
 
379
  print(
380
  "[BlueMagpie] generation policy "
381
  f"name={policy.name} seed={request_seed} generation_cps={generation_cps:.2f} "
382
+ f"duration_units={endpoint_duration_units} "
383
+ f"duration_counter={'network_conservative' if network_conditioned else 'public'} "
384
+ f"expected_steps={expected_steps} hard_stop_steps={hard_stop_steps} "
385
+ f"min_len={min_len}"
386
  )
387
  audio = audio.detach().float().cpu().numpy().reshape(-1)
388
  pace_speed = target_pace_speed(
 
900
  )
901
 
902
 
903
+ def _verify_network_local_asr_intersection(
904
+ turbo_verification,
905
+ trajectory: tuple[np.ndarray, ...],
906
+ chunks: tuple[str, ...],
907
+ anchor: np.ndarray,
908
+ proof_rows: tuple[tuple[NetworkFragmentProof, ...], ...] | None,
909
+ *,
910
+ candidate_seed: int,
911
+ ):
912
+ """Require pinned large-v3 semantics before retaining network locals.
913
+
914
+ Only proof-bearing rows are sent through the independent model. Their
915
+ exact ``NetworkFragmentProof`` tuples are reused unchanged, so neither ASR
916
+ can borrow a whole URL/email proof for a different local range. The
917
+ intersection helper keeps turbo speaker/SQUIM/pace/artifact evidence while
918
+ projecting every independent rejection into a hard semantic local failure.
919
+ """
920
+
921
+ if proof_rows is None:
922
+ return turbo_verification, tuple(
923
+ LocalIndependentGateEvidence(False, None, 0, None)
924
+ for _ in chunks
925
+ )
926
+ if (
927
+ len(proof_rows) != len(chunks)
928
+ or len(trajectory) != len(chunks)
929
+ or len(turbo_verification.candidate_results) != len(chunks)
930
+ ):
931
+ raise ValueError("network local verification provenance does not align")
932
+ independent_evidence = [
933
+ LocalIndependentGateEvidence(
934
+ attempted=False,
935
+ passed=None,
936
+ proof_count=len(proofs),
937
+ result=None,
938
+ )
939
+ for proofs in proof_rows
940
+ ]
941
+ selected_indices = tuple(
942
+ index
943
+ for index, proofs in enumerate(proof_rows)
944
+ if (
945
+ proofs
946
+ and local_candidate_has_coverage_eligibility(
947
+ turbo_verification.candidate_results[index],
948
+ max_local_boundary_speaker_drop=(
949
+ SEQUENCE_FALLBACK_MAX_LOCAL_BOUNDARY_SPEAKER_DROP
950
+ ),
951
+ )
952
+ )
953
+ )
954
+ if not selected_indices:
955
+ return turbo_verification, tuple(independent_evidence)
956
+ independent_proof_rows = tuple(
957
+ proof_rows[index] for index in selected_indices
958
+ )
959
+ independent_verification = _verify_trajectory_audio(
960
+ tuple(trajectory[index] for index in selected_indices),
961
+ tuple(chunks[index] for index in selected_indices),
962
+ anchor,
963
+ 1.0,
964
+ QUALITY_FINAL_ASR_MAX_NEW_TOKENS,
965
+ transcriber=transcribe_verification_whisper,
966
+ semantic_only=True,
967
+ network_fragment_proofs=independent_proof_rows,
968
+ )
969
+ for local_index, primary_index in enumerate(selected_indices):
970
+ result = independent_verification.candidate_results[local_index]
971
+ independent_evidence[primary_index] = LocalIndependentGateEvidence(
972
+ attempted=True,
973
+ passed=result.passed is True,
974
+ proof_count=len(proof_rows[primary_index]),
975
+ result=candidate_gate_evidence(result),
976
+ )
977
+ comparison = result.comparison
978
+ print(
979
+ "[BlueMagpie] network local independent "
980
+ f"seed={candidate_seed} local_chunk_index={primary_index} "
981
+ f"proof_count={len(proof_rows[primary_index])} "
982
+ f"passed={result.passed} cer={comparison.cer:.6f} "
983
+ f"prefix_cer={comparison.prefix_cer:.6f} "
984
+ f"suffix_cer={comparison.suffix_cer:.6f} "
985
+ f"tail_units={comparison.extra_tail_units} "
986
+ f"reasons={result.rejection_reasons}"
987
+ )
988
+ return (
989
+ intersect_local_semantic_verification(
990
+ turbo_verification,
991
+ independent_verification,
992
+ selected_indices,
993
+ ),
994
+ tuple(independent_evidence),
995
+ )
996
+
997
+
998
  def _qualify_candidate_trajectory_audio(
999
  trajectory: tuple[np.ndarray, ...],
1000
  chunks: tuple[str, ...],
 
1008
  ):
1009
  """Run whole-output qualification only after every local chunk passes."""
1010
 
1011
+ proof_rows = _network_fragment_proof_rows(chunks, chunk_specs)
1012
  local_verification = _verify_trajectory_audio(
1013
  trajectory,
1014
  chunks,
1015
  anchor,
1016
  playback_speed,
1017
+ network_fragment_proofs=proof_rows,
1018
  local_endpoint_roles=_local_endpoint_role_rows(chunks, chunk_specs),
1019
  )
1020
+ local_verification, independent_local_results = (
1021
+ _verify_network_local_asr_intersection(
1022
+ local_verification,
1023
+ trajectory,
1024
+ chunks,
1025
+ anchor,
1026
+ proof_rows,
1027
+ candidate_seed=candidate_seed,
1028
+ )
1029
+ )
1030
  if not local_verification.passed:
1031
+ return CandidateVerification(
1032
+ local_verification,
1033
+ independent_local_results=independent_local_results,
1034
+ )
1035
 
1036
  waveform = _assemble_trajectory_audio(
1037
  trajectory,
 
1060
  )
1061
  return CandidateVerification(
1062
  qualified,
1063
+ independent_local_results=independent_local_results,
1064
  joined_output=joined_evidence,
1065
  )
1066
 
 
1082
  )
1083
  return CandidateVerification(
1084
  dual_qualified,
1085
+ independent_local_results=independent_local_results,
1086
  joined_output=joined_evidence,
1087
  independent_output=trajectory_gate_evidence(independent_verification),
1088
  )
 
1094
  anchor: np.ndarray,
1095
  playback_speed: float,
1096
  chunk_specs: tuple[GenerationChunkSpec, ...] | None = None,
1097
+ *,
1098
+ candidate_seed: int,
1099
  ):
1100
+ """Apply strict dual-ASR local gates to one safe-duration refill."""
1101
 
1102
  if len(trajectory) != 1 or len(chunks) != 1:
1103
  return verify_trajectory(())
1104
+ proof_rows = _network_fragment_proof_rows(chunks, chunk_specs)
1105
+ local_verification = _verify_trajectory_audio(
1106
  trajectory,
1107
  chunks,
1108
  anchor,
1109
  playback_speed,
1110
+ network_fragment_proofs=proof_rows,
1111
  local_endpoint_roles=_local_endpoint_role_rows(chunks, chunk_specs),
1112
  )
1113
+ local_verification, independent_local_results = (
1114
+ _verify_network_local_asr_intersection(
1115
+ local_verification,
1116
+ trajectory,
1117
+ chunks,
1118
+ anchor,
1119
+ proof_rows,
1120
+ candidate_seed=candidate_seed,
1121
+ )
1122
+ )
1123
+ return CandidateVerification(
1124
+ local_verification,
1125
+ independent_local_results=independent_local_results,
1126
+ )
1127
 
1128
 
1129
  def _verify_sequence_trajectory_audio(
 
1473
  anchor,
1474
  speed,
1475
  generation_chunk_specs(seed, candidate_chunks),
1476
+ candidate_seed=seed,
1477
  )
1478
  ),
1479
  sequence_final_verifier=lambda sequence_result, candidate_chunks: (
production.py CHANGED
@@ -2118,6 +2118,39 @@ def count_speech_units(text: str) -> int:
2118
  return units
2119
 
2120
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2121
  def effective_generation_cfg(
2122
  text: str,
2123
  requested_cfg: float,
@@ -2268,6 +2301,7 @@ def endpoint_generation_plan(
2268
  step_seconds: float | None,
2269
  margin_steps: int = 1,
2270
  add_terminal_punctuation: bool = True,
 
2271
  ) -> tuple[str, int, int]:
2272
  """Plan a native-pace generation cap independently of output playback pace."""
2273
 
@@ -2276,7 +2310,38 @@ def endpoint_generation_plan(
2276
  if add_terminal_punctuation
2277
  else normalize_tts_text(text)
2278
  )
2279
- expected_steps = target_cps_steps(model_text, generation_cps, step_seconds)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2280
  hard_stop_steps = duration_hard_stop_steps(
2281
  expected_steps,
2282
  ratio=1.0,
 
2118
  return units
2119
 
2120
 
2121
+ def count_network_endpoint_duration_units(text: str) -> int:
2122
+ """Count conservative duration units for network-conditioned generation.
2123
+
2124
+ Public pace, chunk planning and generation-work budgets intentionally keep
2125
+ using :func:`count_speech_units`. The acoustic model needs more native
2126
+ endpoint headroom for opaque URL/email labels, though, so this separate
2127
+ counter compresses every contiguous ASCII alphanumeric run by two instead
2128
+ of compressing alphabetic runs by four. It is valid only for the
2129
+ network-conditioned endpoint estimate and never imposes a minimum length.
2130
+ """
2131
+
2132
+ units = 0
2133
+ ascii_run_length = 0
2134
+
2135
+ def flush_ascii() -> None:
2136
+ nonlocal units, ascii_run_length
2137
+ if ascii_run_length <= 0:
2138
+ return
2139
+ units += max(1, math.ceil(ascii_run_length / 2))
2140
+ ascii_run_length = 0
2141
+
2142
+ for char in normalize_tts_text(text).lower():
2143
+ if char.isascii() and char.isalnum():
2144
+ ascii_run_length += 1
2145
+ elif _is_cjk(char):
2146
+ flush_ascii()
2147
+ units += 1
2148
+ else:
2149
+ flush_ascii()
2150
+ flush_ascii()
2151
+ return units
2152
+
2153
+
2154
  def effective_generation_cfg(
2155
  text: str,
2156
  requested_cfg: float,
 
2301
  step_seconds: float | None,
2302
  margin_steps: int = 1,
2303
  add_terminal_punctuation: bool = True,
2304
+ duration_units: int | None = None,
2305
  ) -> tuple[str, int, int]:
2306
  """Plan a native-pace generation cap independently of output playback pace."""
2307
 
 
2310
  if add_terminal_punctuation
2311
  else normalize_tts_text(text)
2312
  )
2313
+ if duration_units is None:
2314
+ expected_steps = target_cps_steps(
2315
+ model_text,
2316
+ generation_cps,
2317
+ step_seconds,
2318
+ )
2319
+ else:
2320
+ if isinstance(duration_units, bool):
2321
+ raise ValueError("duration_units must be a non-negative integer")
2322
+ try:
2323
+ planned_units = operator.index(duration_units)
2324
+ except (TypeError, ValueError, OverflowError) as error:
2325
+ raise ValueError(
2326
+ "duration_units must be a non-negative integer"
2327
+ ) from error
2328
+ if planned_units < 0:
2329
+ raise ValueError("duration_units must be a non-negative integer")
2330
+ expected_steps = (
2331
+ 0
2332
+ if (
2333
+ planned_units <= 0
2334
+ or generation_cps <= 0.0
2335
+ or not step_seconds
2336
+ or step_seconds <= 0.0
2337
+ )
2338
+ else max(
2339
+ 1,
2340
+ math.ceil(
2341
+ (planned_units / generation_cps) / step_seconds
2342
+ ),
2343
+ )
2344
+ )
2345
  hard_stop_steps = duration_hard_stop_steps(
2346
  expected_steps,
2347
  ratio=1.0,
quality_runtime.py CHANGED
@@ -1561,6 +1561,16 @@ class TrajectoryGateEvidence:
1561
  result: CandidateGateEvidence | None
1562
 
1563
 
 
 
 
 
 
 
 
 
 
 
1564
  @dataclass(frozen=True)
1565
  class CandidateGenerationEvidence:
1566
  """Content-free CFG and source-row evidence for one generation call."""
@@ -1592,6 +1602,7 @@ class CandidateAttemptEvidence:
1592
  scheduled_cfg: float | None
1593
  effective_cfgs: tuple[float, ...]
1594
  floor_reasons: tuple[tuple[str, ...], ...]
 
1595
  joined_output: TrajectoryGateEvidence | None = None
1596
  independent_output: TrajectoryGateEvidence | None = None
1597
 
@@ -2008,6 +2019,7 @@ class CandidateVerification:
2008
  """Selection result plus content-free whole-output gate evidence."""
2009
 
2010
  verification: TrajectoryGateResult
 
2011
  joined_output: TrajectoryGateEvidence | None = None
2012
  independent_output: TrajectoryGateEvidence | None = None
2013
 
@@ -2264,6 +2276,114 @@ def qualify_trajectory_with_joined_output(
2264
  )
2265
 
2266
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2267
  class NoQualifiedCandidateError(RuntimeError):
2268
  """Raised when the full adaptive cascade has no verified trajectory."""
2269
 
@@ -2364,6 +2484,21 @@ def _trajectory_gate_evidence_payload(
2364
  }
2365
 
2366
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2367
  def _candidate_attempt_evidence_payload(
2368
  evidence: CandidateAttemptEvidence,
2369
  ) -> dict[str, Any]:
@@ -2399,6 +2534,14 @@ def _candidate_attempt_evidence_payload(
2399
  _candidate_gate_evidence_payload(result)
2400
  for result in evidence.local_results
2401
  ],
 
 
 
 
 
 
 
 
2402
  "joined_output": _trajectory_gate_evidence_payload(
2403
  evidence.joined_output
2404
  ),
@@ -3045,6 +3188,7 @@ class _VerifiedTrajectoryCandidate:
3045
  seed: int
3046
  trajectory: Any
3047
  verification: TrajectoryGateResult
 
3048
  joined_output: TrajectoryGateEvidence | None = None
3049
  independent_output: TrajectoryGateEvidence | None = None
3050
  generation_evidence: CandidateGenerationEvidence | None = None
@@ -3054,19 +3198,54 @@ def _unwrap_candidate_verification(
3054
  value: Any,
3055
  ) -> tuple[
3056
  TrajectoryGateResult,
 
3057
  TrajectoryGateEvidence | None,
3058
  TrajectoryGateEvidence | None,
3059
  ]:
3060
  if isinstance(value, CandidateVerification):
3061
  verification = value.verification
 
3062
  joined_output = value.joined_output
3063
  independent_output = value.independent_output
3064
  else:
3065
  verification = value
 
3066
  joined_output = None
3067
  independent_output = None
3068
  if not isinstance(verification, TrajectoryGateResult):
3069
  raise TypeError("candidate_verifier must return TrajectoryGateResult")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3070
  if joined_output is not None and not isinstance(
3071
  joined_output,
3072
  TrajectoryGateEvidence,
@@ -3077,7 +3256,12 @@ def _unwrap_candidate_verification(
3077
  TrajectoryGateEvidence,
3078
  ):
3079
  raise TypeError("candidate independent-output evidence is invalid")
3080
- return verification, joined_output, independent_output
 
 
 
 
 
3081
 
3082
 
3083
  def _validated_candidate_generation_evidence(
@@ -3233,6 +3417,7 @@ def _candidate_attempt_evidence(
3233
  scheduled_cfg=(generation.scheduled_cfg if generation is not None else None),
3234
  effective_cfgs=(generation.effective_cfgs if generation is not None else ()),
3235
  floor_reasons=(generation.floor_reasons if generation is not None else ()),
 
3236
  joined_output=candidate.joined_output,
3237
  independent_output=candidate.independent_output,
3238
  )
@@ -3541,6 +3726,27 @@ def _sequence_fallback_candidate_result(
3541
  )
3542
 
3543
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3544
  def _preferred_speaker_verification(
3545
  verification: TrajectoryGateResult,
3546
  *,
@@ -4140,6 +4346,7 @@ def run_coverage_adaptive_cascade(
4140
  try:
4141
  (
4142
  initial_verification,
 
4143
  initial_joined_output,
4144
  initial_independent_output,
4145
  ) = (
@@ -4160,6 +4367,7 @@ def run_coverage_adaptive_cascade(
4160
  seed=base_seed,
4161
  trajectory=initial_trajectory,
4162
  verification=initial_verification,
 
4163
  joined_output=initial_joined_output,
4164
  independent_output=initial_independent_output,
4165
  generation_evidence=initial_generation_evidence,
@@ -4303,6 +4511,7 @@ def run_coverage_adaptive_cascade(
4303
  try:
4304
  (
4305
  refill_verification,
 
4306
  refill_joined_output,
4307
  refill_independent_output,
4308
  ) = (
@@ -4323,6 +4532,7 @@ def run_coverage_adaptive_cascade(
4323
  seed=seed,
4324
  trajectory=refill_trajectory,
4325
  verification=refill_verification,
 
4326
  joined_output=refill_joined_output,
4327
  independent_output=refill_independent_output,
4328
  generation_evidence=refill_generation_evidence,
@@ -4630,6 +4840,7 @@ def run_adaptive_cascade(
4630
  first_trajectory = candidate_generator(chunk_tuple, first_seed)
4631
  (
4632
  first_verification,
 
4633
  first_joined_output,
4634
  first_independent_output,
4635
  ) = _unwrap_candidate_verification(
@@ -4641,6 +4852,7 @@ def run_adaptive_cascade(
4641
  seed=first_seed,
4642
  trajectory=first_trajectory,
4643
  verification=first_verification,
 
4644
  joined_output=first_joined_output,
4645
  independent_output=first_independent_output,
4646
  )
@@ -4677,6 +4889,7 @@ def run_adaptive_cascade(
4677
  trajectory = candidate_generator(chunk_tuple, seed)
4678
  (
4679
  verification,
 
4680
  joined_output,
4681
  independent_output,
4682
  ) = _unwrap_candidate_verification(
@@ -4689,6 +4902,7 @@ def run_adaptive_cascade(
4689
  seed=seed,
4690
  trajectory=trajectory,
4691
  verification=verification,
 
4692
  joined_output=joined_output,
4693
  independent_output=independent_output,
4694
  )
 
1561
  result: CandidateGateEvidence | None
1562
 
1563
 
1564
+ @dataclass(frozen=True)
1565
+ class LocalIndependentGateEvidence:
1566
+ """Bounded per-row attestation for independent local semantic ASR."""
1567
+
1568
+ attempted: bool
1569
+ passed: bool | None
1570
+ proof_count: int
1571
+ result: CandidateGateEvidence | None
1572
+
1573
+
1574
  @dataclass(frozen=True)
1575
  class CandidateGenerationEvidence:
1576
  """Content-free CFG and source-row evidence for one generation call."""
 
1602
  scheduled_cfg: float | None
1603
  effective_cfgs: tuple[float, ...]
1604
  floor_reasons: tuple[tuple[str, ...], ...]
1605
+ independent_local_results: tuple[LocalIndependentGateEvidence, ...] = ()
1606
  joined_output: TrajectoryGateEvidence | None = None
1607
  independent_output: TrajectoryGateEvidence | None = None
1608
 
 
2019
  """Selection result plus content-free whole-output gate evidence."""
2020
 
2021
  verification: TrajectoryGateResult
2022
+ independent_local_results: tuple[LocalIndependentGateEvidence, ...] = ()
2023
  joined_output: TrajectoryGateEvidence | None = None
2024
  independent_output: TrajectoryGateEvidence | None = None
2025
 
 
2276
  )
2277
 
2278
 
2279
+ def intersect_local_semantic_verification(
2280
+ primary_verification: TrajectoryGateResult,
2281
+ independent_verification: TrajectoryGateResult,
2282
+ primary_indices: Sequence[int],
2283
+ ) -> TrajectoryGateResult:
2284
+ """Hard-intersect selected local rows without replacing acoustic evidence.
2285
+
2286
+ ``primary_verification`` owns the turbo ASR, speaker, SQUIM, pace and
2287
+ boundary-proxy evidence consumed by the coverage selector.
2288
+ ``independent_verification`` contains semantic-only large-v3 results for
2289
+ the selected network-conditioned rows in the order given by
2290
+ ``primary_indices``. A large-v3 rejection is projected onto the
2291
+ corresponding primary row with allow-listed semantic reason codes, making
2292
+ it impossible for either the strict pool or the boundary-only proxy to
2293
+ retain that local candidate. Passing intersections return the original
2294
+ primary object unchanged.
2295
+ """
2296
+
2297
+ if not isinstance(primary_verification, TrajectoryGateResult):
2298
+ raise TypeError("primary_verification must be a TrajectoryGateResult")
2299
+ if not isinstance(independent_verification, TrajectoryGateResult):
2300
+ raise TypeError(
2301
+ "independent_verification must be a TrajectoryGateResult"
2302
+ )
2303
+ try:
2304
+ selected_indices = tuple(primary_indices)
2305
+ except TypeError as error:
2306
+ raise ValueError("primary_indices must be an ordered index sequence") from error
2307
+ if (
2308
+ not selected_indices
2309
+ or any(
2310
+ isinstance(index, (bool, np.bool_))
2311
+ or not isinstance(index, int)
2312
+ for index in selected_indices
2313
+ )
2314
+ or selected_indices != tuple(sorted(set(selected_indices)))
2315
+ or any(
2316
+ index < 0
2317
+ or index >= len(primary_verification.candidate_results)
2318
+ for index in selected_indices
2319
+ )
2320
+ or len(independent_verification.candidate_results)
2321
+ != len(selected_indices)
2322
+ or any(
2323
+ not isinstance(result, CandidateGateResult)
2324
+ for result in independent_verification.candidate_results
2325
+ )
2326
+ ):
2327
+ raise ValueError(
2328
+ "independent local verification rows do not match primary indices"
2329
+ )
2330
+
2331
+ failed_secondary_rows = {
2332
+ local_index
2333
+ for local_index, result in enumerate(
2334
+ independent_verification.candidate_results
2335
+ )
2336
+ if (
2337
+ result.passed is not True
2338
+ or not math.isfinite(result.score)
2339
+ or bool(result.rejection_reasons)
2340
+ )
2341
+ }
2342
+ if independent_verification.passed is not True and not failed_secondary_rows:
2343
+ # A malformed/non-finite trajectory-level result must not allow any of
2344
+ # its apparently passing local projections into the coverage pool.
2345
+ failed_secondary_rows.update(range(len(selected_indices)))
2346
+ if not failed_secondary_rows:
2347
+ return primary_verification
2348
+
2349
+ combined_results = list(primary_verification.candidate_results)
2350
+ for local_index in sorted(failed_secondary_rows):
2351
+ primary_index = selected_indices[local_index]
2352
+ primary_result = combined_results[primary_index]
2353
+ independent_result = independent_verification.candidate_results[
2354
+ local_index
2355
+ ]
2356
+ semantic_reasons = ["semantic_gate"]
2357
+ if (
2358
+ "network_protected_span_mismatch"
2359
+ in independent_result.rejection_reasons
2360
+ ):
2361
+ semantic_reasons.append("network_protected_span_mismatch")
2362
+ combined_results[primary_index] = replace(
2363
+ primary_result,
2364
+ passed=False,
2365
+ score=math.inf,
2366
+ rejection_reasons=tuple(
2367
+ dict.fromkeys(
2368
+ (*primary_result.rejection_reasons, *semantic_reasons)
2369
+ )
2370
+ ),
2371
+ )
2372
+
2373
+ rejection_reasons = tuple(
2374
+ f"chunk_{index}:{reason}"
2375
+ for index, result in enumerate(combined_results)
2376
+ for reason in result.rejection_reasons
2377
+ )
2378
+ return TrajectoryGateResult(
2379
+ passed=False,
2380
+ candidate_results=tuple(combined_results),
2381
+ score=math.inf,
2382
+ rejection_reasons=rejection_reasons,
2383
+ chunk_artifacts=primary_verification.chunk_artifacts,
2384
+ )
2385
+
2386
+
2387
  class NoQualifiedCandidateError(RuntimeError):
2388
  """Raised when the full adaptive cascade has no verified trajectory."""
2389
 
 
2484
  }
2485
 
2486
 
2487
+ def _local_independent_gate_evidence_payload(
2488
+ evidence: LocalIndependentGateEvidence,
2489
+ ) -> dict[str, Any]:
2490
+ return {
2491
+ "attempted": evidence.attempted,
2492
+ "passed": evidence.passed,
2493
+ "proof_count": evidence.proof_count,
2494
+ "result": (
2495
+ None
2496
+ if evidence.result is None
2497
+ else _candidate_gate_evidence_payload(evidence.result)
2498
+ ),
2499
+ }
2500
+
2501
+
2502
  def _candidate_attempt_evidence_payload(
2503
  evidence: CandidateAttemptEvidence,
2504
  ) -> dict[str, Any]:
 
2534
  _candidate_gate_evidence_payload(result)
2535
  for result in evidence.local_results
2536
  ],
2537
+ "independent_local_evidence_complete": (
2538
+ len(evidence.independent_local_results)
2539
+ == evidence.local_result_count
2540
+ ),
2541
+ "independent_local_results": [
2542
+ _local_independent_gate_evidence_payload(result)
2543
+ for result in evidence.independent_local_results
2544
+ ],
2545
  "joined_output": _trajectory_gate_evidence_payload(
2546
  evidence.joined_output
2547
  ),
 
3188
  seed: int
3189
  trajectory: Any
3190
  verification: TrajectoryGateResult
3191
+ independent_local_results: tuple[LocalIndependentGateEvidence, ...] = ()
3192
  joined_output: TrajectoryGateEvidence | None = None
3193
  independent_output: TrajectoryGateEvidence | None = None
3194
  generation_evidence: CandidateGenerationEvidence | None = None
 
3198
  value: Any,
3199
  ) -> tuple[
3200
  TrajectoryGateResult,
3201
+ tuple[LocalIndependentGateEvidence, ...],
3202
  TrajectoryGateEvidence | None,
3203
  TrajectoryGateEvidence | None,
3204
  ]:
3205
  if isinstance(value, CandidateVerification):
3206
  verification = value.verification
3207
+ independent_local_results = value.independent_local_results
3208
  joined_output = value.joined_output
3209
  independent_output = value.independent_output
3210
  else:
3211
  verification = value
3212
+ independent_local_results = ()
3213
  joined_output = None
3214
  independent_output = None
3215
  if not isinstance(verification, TrajectoryGateResult):
3216
  raise TypeError("candidate_verifier must return TrajectoryGateResult")
3217
+ if not isinstance(independent_local_results, tuple) or (
3218
+ independent_local_results
3219
+ and (
3220
+ len(independent_local_results)
3221
+ != len(verification.candidate_results)
3222
+ or any(
3223
+ not isinstance(result, LocalIndependentGateEvidence)
3224
+ or type(result.attempted) is not bool
3225
+ or type(result.proof_count) is not int
3226
+ or result.proof_count > CASCADE_EVIDENCE_MAX_LOCAL_RESULTS
3227
+ or (
3228
+ result.attempted
3229
+ and (
3230
+ type(result.passed) is not bool
3231
+ or result.proof_count <= 0
3232
+ or not isinstance(result.result, CandidateGateEvidence)
3233
+ or result.result.passed is not result.passed
3234
+ )
3235
+ )
3236
+ or (
3237
+ not result.attempted
3238
+ and (
3239
+ result.passed is not None
3240
+ or result.proof_count < 0
3241
+ or result.result is not None
3242
+ )
3243
+ )
3244
+ for result in independent_local_results
3245
+ )
3246
+ )
3247
+ ):
3248
+ raise TypeError("candidate independent-local evidence is invalid")
3249
  if joined_output is not None and not isinstance(
3250
  joined_output,
3251
  TrajectoryGateEvidence,
 
3256
  TrajectoryGateEvidence,
3257
  ):
3258
  raise TypeError("candidate independent-output evidence is invalid")
3259
+ return (
3260
+ verification,
3261
+ independent_local_results,
3262
+ joined_output,
3263
+ independent_output,
3264
+ )
3265
 
3266
 
3267
  def _validated_candidate_generation_evidence(
 
3417
  scheduled_cfg=(generation.scheduled_cfg if generation is not None else None),
3418
  effective_cfgs=(generation.effective_cfgs if generation is not None else ()),
3419
  floor_reasons=(generation.floor_reasons if generation is not None else ()),
3420
+ independent_local_results=candidate.independent_local_results,
3421
  joined_output=candidate.joined_output,
3422
  independent_output=candidate.independent_output,
3423
  )
 
3726
  )
3727
 
3728
 
3729
+ def local_candidate_has_coverage_eligibility(
3730
+ result: CandidateGateResult,
3731
+ *,
3732
+ max_local_boundary_speaker_drop: float | None,
3733
+ ) -> bool:
3734
+ """Return whether a primary local result could enter either safe pool."""
3735
+
3736
+ if not isinstance(result, CandidateGateResult):
3737
+ return False
3738
+ eligible = _sequence_fallback_candidate_result(
3739
+ result,
3740
+ max_local_boundary_speaker_drop=max_local_boundary_speaker_drop,
3741
+ )
3742
+ return bool(
3743
+ eligible.passed is True
3744
+ and math.isfinite(eligible.score)
3745
+ and eligible.score >= 0.0
3746
+ and not eligible.rejection_reasons
3747
+ )
3748
+
3749
+
3750
  def _preferred_speaker_verification(
3751
  verification: TrajectoryGateResult,
3752
  *,
 
4346
  try:
4347
  (
4348
  initial_verification,
4349
+ initial_independent_local_results,
4350
  initial_joined_output,
4351
  initial_independent_output,
4352
  ) = (
 
4367
  seed=base_seed,
4368
  trajectory=initial_trajectory,
4369
  verification=initial_verification,
4370
+ independent_local_results=initial_independent_local_results,
4371
  joined_output=initial_joined_output,
4372
  independent_output=initial_independent_output,
4373
  generation_evidence=initial_generation_evidence,
 
4511
  try:
4512
  (
4513
  refill_verification,
4514
+ refill_independent_local_results,
4515
  refill_joined_output,
4516
  refill_independent_output,
4517
  ) = (
 
4532
  seed=seed,
4533
  trajectory=refill_trajectory,
4534
  verification=refill_verification,
4535
+ independent_local_results=refill_independent_local_results,
4536
  joined_output=refill_joined_output,
4537
  independent_output=refill_independent_output,
4538
  generation_evidence=refill_generation_evidence,
 
4840
  first_trajectory = candidate_generator(chunk_tuple, first_seed)
4841
  (
4842
  first_verification,
4843
+ first_independent_local_results,
4844
  first_joined_output,
4845
  first_independent_output,
4846
  ) = _unwrap_candidate_verification(
 
4852
  seed=first_seed,
4853
  trajectory=first_trajectory,
4854
  verification=first_verification,
4855
+ independent_local_results=first_independent_local_results,
4856
  joined_output=first_joined_output,
4857
  independent_output=first_independent_output,
4858
  )
 
4889
  trajectory = candidate_generator(chunk_tuple, seed)
4890
  (
4891
  verification,
4892
+ independent_local_results,
4893
  joined_output,
4894
  independent_output,
4895
  ) = _unwrap_candidate_verification(
 
4902
  seed=seed,
4903
  trajectory=trajectory,
4904
  verification=verification,
4905
+ independent_local_results=independent_local_results,
4906
  joined_output=joined_output,
4907
  independent_output=independent_output,
4908
  )
tests/test_coverage_adaptive.py CHANGED
@@ -11,11 +11,14 @@ from quality_runtime import (
11
  CandidateObservation,
12
  CandidateVerification,
13
  ChunkCandidateArtifact,
 
14
  NoQualifiedCandidateError,
15
  SEQUENCE_FALLBACK_MAX_LOCAL_BOUNDARY_SPEAKER_DROP,
16
  TrajectoryGateResult,
 
17
  format_cascade_evidence_log,
18
  generation_cfg_for_candidate_offset,
 
19
  run_coverage_adaptive_cascade,
20
  select_culprit_diverse_candidate_sequences,
21
  trajectory_gate_evidence,
@@ -161,6 +164,142 @@ def test_whole_exact_winner_returns_before_any_refill_or_sequence_check():
161
  assert result.diagnostics.attempts[0].joined_output.passed
162
 
163
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  def test_single_chunk_requires_joint_preferred_quality_and_speaker_for_early_return():
165
  chunk = ("完整內容",)
166
  generated = []
 
11
  CandidateObservation,
12
  CandidateVerification,
13
  ChunkCandidateArtifact,
14
+ LocalIndependentGateEvidence,
15
  NoQualifiedCandidateError,
16
  SEQUENCE_FALLBACK_MAX_LOCAL_BOUNDARY_SPEAKER_DROP,
17
  TrajectoryGateResult,
18
+ candidate_gate_evidence,
19
  format_cascade_evidence_log,
20
  generation_cfg_for_candidate_offset,
21
+ intersect_local_semantic_verification,
22
  run_coverage_adaptive_cascade,
23
  select_culprit_diverse_candidate_sequences,
24
  trajectory_gate_evidence,
 
164
  assert result.diagnostics.attempts[0].joined_output.passed
165
 
166
 
167
+ def test_mixed_network_local_v3_failure_cannot_enter_strict_or_proxy_pool():
168
+ chunks = ("普通內容完整", "網路內容完整")
169
+ generated = []
170
+
171
+ def primary(candidate_chunks):
172
+ observations = []
173
+ artifacts = []
174
+ for index, chunk in enumerate(candidate_chunks):
175
+ observations.append(
176
+ CandidateObservation(
177
+ target_text=chunk,
178
+ transcript_text=chunk,
179
+ audio_duration_seconds=2.0,
180
+ speaker_similarity=0.70,
181
+ begin_speaker_similarity=(0.80 if index == len(candidate_chunks) - 1 else 0.60),
182
+ end_speaker_similarity=(0.68 if index == len(candidate_chunks) - 1 else 0.60),
183
+ pace_cps=2.0,
184
+ )
185
+ )
186
+ artifacts.append(
187
+ ChunkCandidateArtifact(
188
+ speaker_embedding=np.array([1.0, 0.0], dtype=np.float32),
189
+ rms_db=-20.0,
190
+ )
191
+ )
192
+ return verify_trajectory(
193
+ observations,
194
+ chunk_artifacts=artifacts,
195
+ max_pace_cps=4.3,
196
+ max_boundary_speaker_drop=0.10,
197
+ )
198
+
199
+ def independent_failure(target):
200
+ return verify_trajectory(
201
+ [CandidateObservation(target, "完全錯誤", 2.0)],
202
+ speaker_gate_enabled=False,
203
+ )
204
+
205
+ def combined(candidate_chunks):
206
+ primary_verification = primary(candidate_chunks)
207
+ network_local_index = len(candidate_chunks) - 1
208
+ independent = independent_failure(
209
+ candidate_chunks[network_local_index]
210
+ )
211
+ verification = intersect_local_semantic_verification(
212
+ primary_verification,
213
+ independent,
214
+ (network_local_index,),
215
+ )
216
+ local_evidence = tuple(
217
+ (
218
+ LocalIndependentGateEvidence(
219
+ True,
220
+ False,
221
+ 1,
222
+ candidate_gate_evidence(
223
+ independent.candidate_results[0]
224
+ ),
225
+ )
226
+ if index == network_local_index
227
+ else LocalIndependentGateEvidence(False, None, 0, None)
228
+ )
229
+ for index in range(len(candidate_chunks))
230
+ )
231
+ return CandidateVerification(
232
+ verification,
233
+ independent_local_results=local_evidence,
234
+ )
235
+
236
+ def generator(candidate_chunks, seed):
237
+ generated.append((candidate_chunks, seed))
238
+ return tuple(f"{seed}:{chunk}" for chunk in candidate_chunks)
239
+
240
+ with pytest.raises(NoQualifiedCandidateError) as captured:
241
+ run_coverage_adaptive_cascade(
242
+ chunks,
243
+ 800,
244
+ generator,
245
+ lambda trajectory, candidate_chunks, seed: combined(
246
+ candidate_chunks
247
+ ),
248
+ lambda trajectory, candidate_chunks, seed: combined(
249
+ candidate_chunks
250
+ ),
251
+ sequence_final_verifier=lambda result, candidate_chunks: pytest.fail(
252
+ "a full path must not exist"
253
+ ),
254
+ max_generated_chunks=3,
255
+ max_generated_text_units=100,
256
+ sequence_fallback_max_local_boundary_speaker_drop=(
257
+ SEQUENCE_FALLBACK_MAX_LOCAL_BOUNDARY_SPEAKER_DROP
258
+ ),
259
+ )
260
+
261
+ assert generated == [
262
+ (chunks, 800),
263
+ ((chunks[1],), 801),
264
+ ]
265
+ diagnostics = captured.value.diagnostics
266
+ assert len(diagnostics.attempts) == 2
267
+ assert diagnostics.sequence_search is not None
268
+ assert diagnostics.sequence_search.eligible_candidate_counts == (1, 0)
269
+ initial = diagnostics.attempts[0]
270
+ refill = diagnostics.attempts[1]
271
+ assert initial.local_results[0].passed is True
272
+ assert initial.local_results[1].passed is False
273
+ assert initial.local_results[1].rejection_reasons == (
274
+ "boundary_speaker_drop",
275
+ "semantic_gate",
276
+ )
277
+ assert initial.independent_local_results[0] == (
278
+ LocalIndependentGateEvidence(False, None, 0, None)
279
+ )
280
+ assert initial.independent_local_results[1].attempted is True
281
+ assert initial.independent_local_results[1].passed is False
282
+ assert refill.independent_local_results[0].attempted is True
283
+ assert refill.independent_local_results[0].passed is False
284
+ payload = json.loads(
285
+ format_cascade_evidence_log(
286
+ diagnostics,
287
+ outcome="no_qualified_candidate",
288
+ generated_chunk_limit=3,
289
+ generated_text_unit_limit=100,
290
+ ).removeprefix(CASCADE_EVIDENCE_LOG_PREFIX)
291
+ )
292
+ assert payload["attempts"][0]["independent_local_results"][0] == {
293
+ "attempted": False,
294
+ "passed": None,
295
+ "proof_count": 0,
296
+ "result": None,
297
+ }
298
+ assert payload["attempts"][0]["independent_local_results"][1][
299
+ "passed"
300
+ ] is False
301
+
302
+
303
  def test_single_chunk_requires_joint_preferred_quality_and_speaker_for_early_return():
304
  chunk = ("完整內容",)
305
  generated = []
tests/test_production.py CHANGED
@@ -1,4 +1,5 @@
1
  import math
 
2
 
3
  import numpy as np
4
  import pytest
@@ -15,6 +16,7 @@ from production import (
15
  coalesce_text_chunks,
16
  compare_asr_text,
17
  contains_network_identifier,
 
18
  count_speech_units,
19
  duration_hard_stop_steps,
20
  effective_generation_cfg,
@@ -1245,6 +1247,44 @@ def test_local_network_fragment_accepts_exact_range_bound_hybrid_lexical_atoms()
1245
  assert evidence.transcript_text == target
1246
 
1247
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1248
  @pytest.mark.parametrize(
1249
  "transcript",
1250
  (
@@ -1929,6 +1969,7 @@ def test_prefaded_network_verifier_seam_preserves_exact_zero_silence():
1929
 
1930
  def test_duration_units_and_target_pace_min_len():
1931
  assert count_speech_units("AI TTS 測試 1234") == 6
 
1932
  assert target_cps_min_len(
1933
  "一二三四五六七八",
1934
  target_cps=4.0,
@@ -1941,6 +1982,60 @@ def test_duration_units_and_target_pace_min_len():
1941
  assert duration_hard_stop_steps(0, ratio=1.08, margin_steps=3) == 2000
1942
 
1943
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1944
  def test_terminal_punctuation_supplies_an_endpoint_cue():
1945
  assert ensure_terminal_punctuation("內容已經說完") == "內容已經說完。"
1946
  assert ensure_terminal_punctuation("內容已經說完!") == "內容已經說完!"
 
1
  import math
2
+ from dataclasses import replace
3
 
4
  import numpy as np
5
  import pytest
 
16
  coalesce_text_chunks,
17
  compare_asr_text,
18
  contains_network_identifier,
19
+ count_network_endpoint_duration_units,
20
  count_speech_units,
21
  duration_hard_stop_steps,
22
  effective_generation_cfg,
 
1247
  assert evidence.transcript_text == target
1248
 
1249
 
1250
+ def test_real_network_fragment_proofs_reject_repeat_wrong_range_and_reorder():
1251
+ raw = "請查 https://a.tw 並寄信到 a@b.co。"
1252
+ spec = plan_generation_chunks(raw, normalize_spoken_forms(raw))[0]
1253
+ first, second = spec.network_fragment_proofs
1254
+
1255
+ exact = canonicalize_asr_network_fragments(
1256
+ spec.text,
1257
+ spec.text,
1258
+ (first, second),
1259
+ )
1260
+
1261
+ assert exact.passed is True
1262
+ with pytest.raises(ValueError, match="bind|offset|range"):
1263
+ canonicalize_asr_network_fragments(
1264
+ spec.text,
1265
+ spec.text,
1266
+ (first, first),
1267
+ )
1268
+ with pytest.raises(ValueError, match="bind|offset|range"):
1269
+ canonicalize_asr_network_fragments(
1270
+ spec.text,
1271
+ spec.text,
1272
+ (second, first),
1273
+ )
1274
+ with pytest.raises(ValueError, match="bind|offset|range"):
1275
+ canonicalize_asr_network_fragments(
1276
+ spec.text,
1277
+ spec.text,
1278
+ (
1279
+ replace(
1280
+ first,
1281
+ chunk_start=first.chunk_start + 1,
1282
+ ),
1283
+ second,
1284
+ ),
1285
+ )
1286
+
1287
+
1288
  @pytest.mark.parametrize(
1289
  "transcript",
1290
  (
 
1969
 
1970
  def test_duration_units_and_target_pace_min_len():
1971
  assert count_speech_units("AI TTS 測試 1234") == 6
1972
+ assert count_network_endpoint_duration_units("AI TTS 測試 1234") == 7
1973
  assert target_cps_min_len(
1974
  "一二三四五六七八",
1975
  target_cps=4.0,
 
1982
  assert duration_hard_stop_steps(0, ratio=1.08, margin_steps=3) == 2000
1983
 
1984
 
1985
+ def test_network_endpoint_duration_counter_is_separate_from_public_units():
1986
+ text = " coastwatch 點 example 點 T W 斜線 tide。"
1987
+
1988
+ assert count_speech_units(text) == 12
1989
+ assert count_network_endpoint_duration_units(text) == 17
1990
+ assert count_speech_units("tour123") == 2
1991
+ assert count_network_endpoint_duration_units("tour123") == 4
1992
+
1993
+
1994
+ def test_h07_safe_network_endpoint_uses_conservative_units_only_for_cap():
1995
+ raw = "潮汐預報可查詢 https://coastwatch.example.tw/tide。"
1996
+ normalized = normalize_spoken_forms(raw)
1997
+ specs = plan_generation_chunks(raw, normalized)
1998
+ row = specs[1]
1999
+
2000
+ assert row.network_conditioned
2001
+ assert count_speech_units(row.text) == 12
2002
+ duration_units = count_network_endpoint_duration_units(row.text)
2003
+ model_text, expected_steps, hard_stop_steps = endpoint_generation_plan(
2004
+ row.text,
2005
+ generation_cps=4.0,
2006
+ step_seconds=0.16,
2007
+ margin_steps=1,
2008
+ duration_units=duration_units,
2009
+ )
2010
+
2011
+ assert model_text == normalize_tts_text(row.text)
2012
+ assert duration_units == 17
2013
+ assert expected_steps == 27
2014
+ assert hard_stop_steps == 28
2015
+ # Planner and public pace/work accounting remain on the frozen counter.
2016
+ assert tuple(count_speech_units(spec.text) for spec in specs) == (18, 12)
2017
+ assert target_pace_speed(
2018
+ 28_000,
2019
+ 10_000,
2020
+ row.text,
2021
+ target_cps=4.0,
2022
+ min_speed=0.80,
2023
+ ) == pytest.approx(2.8 / 3.0)
2024
+
2025
+
2026
+ @pytest.mark.parametrize("duration_units", [True, -1, 1.5, "17"])
2027
+ def test_endpoint_generation_plan_rejects_invalid_explicit_duration_units(
2028
+ duration_units,
2029
+ ):
2030
+ with pytest.raises(ValueError, match="duration_units"):
2031
+ endpoint_generation_plan(
2032
+ "測試",
2033
+ generation_cps=4.0,
2034
+ step_seconds=0.16,
2035
+ duration_units=duration_units,
2036
+ )
2037
+
2038
+
2039
  def test_terminal_punctuation_supplies_an_endpoint_cue():
2040
  assert ensure_terminal_punctuation("內容已經說完") == "內容已經說完。"
2041
  assert ensure_terminal_punctuation("內容已經說完!") == "內容已經說完!"
tests/test_quality_runtime.py CHANGED
@@ -7,7 +7,12 @@ import pytest
7
  import soundfile as sf
8
  import torch
9
 
10
- from production import count_speech_units, normalize_spoken_forms, split_text_for_tts
 
 
 
 
 
11
  from quality_runtime import (
12
  ADAPTIVE_CASCADE_STAGE_LIMITS,
13
  BASE_GENERATION_POLICY,
@@ -24,6 +29,7 @@ from quality_runtime import (
24
  WHISPER_MODEL_ID,
25
  WHISPER_RETURN_ATTENTION_MASK,
26
  WHISPER_REVISION,
 
27
  CandidateObservation,
28
  CandidateVerification,
29
  CascadeResult,
@@ -31,6 +37,7 @@ from quality_runtime import (
31
  FinalOutputRejectedError,
32
  LazySquimObjective,
33
  LazyWhisperASR,
 
34
  NoQualifiedCandidateError,
35
  RELEASE_SPEAKER_TRIGGER_SECONDS,
36
  SEQUENCE_FALLBACK_MAX_LOCAL_BOUNDARY_SPEAKER_DROP,
@@ -42,6 +49,7 @@ from quality_runtime import (
42
  _split_whisper_audio,
43
  active_audio_median_f0_hz,
44
  active_voiced_duration_seconds,
 
45
  candidate_chunk_transition_score,
46
  candidate_limit_for_chunk_budget,
47
  cosine_similarity,
@@ -49,6 +57,8 @@ from quality_runtime import (
49
  format_cascade_evidence_log,
50
  generation_cfg_for_candidate_offset,
51
  generation_policy_for_candidate_offset,
 
 
52
  load_pinned_whisper_runtime,
53
  load_pinned_verification_whisper_runtime,
54
  load_pinned_squim_objective_runtime,
@@ -59,6 +69,7 @@ from quality_runtime import (
59
  require_verified_final_output,
60
  resolve_request_seed,
61
  run_adaptive_cascade,
 
62
  select_k_candidate_sequences,
63
  speaker_embedding_from_audio,
64
  speaker_evidence_from_audio,
@@ -1446,6 +1457,271 @@ def test_joined_output_qualification_returns_local_result_only_when_joined_passe
1446
  assert qualify_trajectory_with_joined_output(local, joined) is local
1447
 
1448
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1449
  def test_cascade_evidence_success_is_content_free_canonical_and_behavior_golden():
1450
  private_target = "PRIVATE_TARGET_ALPHA"
1451
  private_transcript = "PRIVATE_TRANSCRIPT_OMEGA"
@@ -2749,6 +3025,51 @@ def test_candidate_limit_obeys_generated_text_unit_budget(text_units, expected_l
2749
  assert limit * text_units <= 800
2750
 
2751
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2752
  def test_candidate_limit_requires_complete_positive_text_unit_budget():
2753
  with pytest.raises(ValueError, match="provided together"):
2754
  candidate_limit_for_chunk_budget(1, total_text_units=40)
 
7
  import soundfile as sf
8
  import torch
9
 
10
+ from production import (
11
+ count_network_endpoint_duration_units,
12
+ count_speech_units,
13
+ normalize_spoken_forms,
14
+ split_text_for_tts,
15
+ )
16
  from quality_runtime import (
17
  ADAPTIVE_CASCADE_STAGE_LIMITS,
18
  BASE_GENERATION_POLICY,
 
29
  WHISPER_MODEL_ID,
30
  WHISPER_RETURN_ATTENTION_MASK,
31
  WHISPER_REVISION,
32
+ CandidateGenerationEvidence,
33
  CandidateObservation,
34
  CandidateVerification,
35
  CascadeResult,
 
37
  FinalOutputRejectedError,
38
  LazySquimObjective,
39
  LazyWhisperASR,
40
+ LocalIndependentGateEvidence,
41
  NoQualifiedCandidateError,
42
  RELEASE_SPEAKER_TRIGGER_SECONDS,
43
  SEQUENCE_FALLBACK_MAX_LOCAL_BOUNDARY_SPEAKER_DROP,
 
49
  _split_whisper_audio,
50
  active_audio_median_f0_hz,
51
  active_voiced_duration_seconds,
52
+ candidate_gate_evidence,
53
  candidate_chunk_transition_score,
54
  candidate_limit_for_chunk_budget,
55
  cosine_similarity,
 
57
  format_cascade_evidence_log,
58
  generation_cfg_for_candidate_offset,
59
  generation_policy_for_candidate_offset,
60
+ intersect_local_semantic_verification,
61
+ local_candidate_has_coverage_eligibility,
62
  load_pinned_whisper_runtime,
63
  load_pinned_verification_whisper_runtime,
64
  load_pinned_squim_objective_runtime,
 
69
  require_verified_final_output,
70
  resolve_request_seed,
71
  run_adaptive_cascade,
72
+ run_coverage_adaptive_cascade,
73
  select_k_candidate_sequences,
74
  speaker_embedding_from_audio,
75
  speaker_evidence_from_audio,
 
1457
  assert qualify_trajectory_with_joined_output(local, joined) is local
1458
 
1459
 
1460
+ def test_independent_local_semantic_reject_preserves_primary_acoustic_evidence():
1461
+ target = normalize_spoken_forms(
1462
+ "https://museum.example.tw/path"
1463
+ )
1464
+ artifact = ChunkCandidateArtifact(
1465
+ speaker_embedding=np.asarray([1.0, 0.0], dtype=np.float32),
1466
+ rms_db=-20.0,
1467
+ median_f0_hz=180.0,
1468
+ )
1469
+ primary = verify_trajectory(
1470
+ [
1471
+ CandidateObservation(
1472
+ target_text=target,
1473
+ transcript_text=target,
1474
+ audio_duration_seconds=2.0,
1475
+ speaker_similarity=0.70,
1476
+ begin_speaker_similarity=0.60,
1477
+ end_speaker_similarity=0.55,
1478
+ pace_cps=4.0,
1479
+ squim_stoi=0.80,
1480
+ squim_pesq=1.50,
1481
+ squim_si_sdr=4.0,
1482
+ )
1483
+ ],
1484
+ chunk_artifacts=(artifact,),
1485
+ max_pace_cps=4.3,
1486
+ squim_gate_enabled=True,
1487
+ min_squim_stoi=0.60,
1488
+ min_squim_pesq=1.12,
1489
+ max_boundary_speaker_drop=0.10,
1490
+ )
1491
+ independent = verify_trajectory(
1492
+ [
1493
+ CandidateObservation(
1494
+ target_text=target,
1495
+ transcript_text=target.replace("museum", "museuman"),
1496
+ audio_duration_seconds=2.0,
1497
+ )
1498
+ ],
1499
+ speaker_gate_enabled=False,
1500
+ )
1501
+
1502
+ combined = intersect_local_semantic_verification(
1503
+ primary,
1504
+ independent,
1505
+ (0,),
1506
+ )
1507
+
1508
+ assert primary.passed
1509
+ assert independent.rejection_reasons == (
1510
+ "chunk_0:semantic_gate",
1511
+ "chunk_0:network_protected_span_mismatch",
1512
+ )
1513
+ assert not combined.passed
1514
+ assert math.isinf(combined.score)
1515
+ result = combined.candidate_results[0]
1516
+ primary_result = primary.candidate_results[0]
1517
+ assert result.comparison is primary_result.comparison
1518
+ assert result.speaker_similarity == primary_result.speaker_similarity
1519
+ assert result.boundary_speaker_drop == primary_result.boundary_speaker_drop
1520
+ assert result.squim_stoi == primary_result.squim_stoi
1521
+ assert result.squim_pesq == primary_result.squim_pesq
1522
+ assert result.squim_si_sdr == primary_result.squim_si_sdr
1523
+ assert result.rejection_reasons == (
1524
+ "semantic_gate",
1525
+ "network_protected_span_mismatch",
1526
+ )
1527
+ assert combined.chunk_artifacts is primary.chunk_artifacts
1528
+ # Boundary-only fallback cannot erase an independent semantic rejection.
1529
+ assert (
1530
+ _sequence_fallback_candidate_result(
1531
+ result,
1532
+ max_local_boundary_speaker_drop=0.15,
1533
+ )
1534
+ is result
1535
+ )
1536
+
1537
+
1538
+ def test_independent_local_semantic_pass_preserves_boundary_proxy_unchanged():
1539
+ target = "這段網路片段內容完整"
1540
+ primary = verify_trajectory(
1541
+ [
1542
+ CandidateObservation(
1543
+ target_text=target,
1544
+ transcript_text=target,
1545
+ audio_duration_seconds=2.0,
1546
+ speaker_similarity=0.70,
1547
+ begin_speaker_similarity=0.80,
1548
+ end_speaker_similarity=0.68,
1549
+ )
1550
+ ],
1551
+ )
1552
+ independent = verify_trajectory(
1553
+ [CandidateObservation(target, target, 2.0)],
1554
+ speaker_gate_enabled=False,
1555
+ )
1556
+
1557
+ combined = intersect_local_semantic_verification(
1558
+ primary,
1559
+ independent,
1560
+ (0,),
1561
+ )
1562
+
1563
+ assert not primary.passed
1564
+ assert primary.rejection_reasons == ("chunk_0:boundary_speaker_drop",)
1565
+ assert independent.passed
1566
+ assert combined is primary
1567
+ relaxed = _sequence_fallback_candidate_result(
1568
+ combined.candidate_results[0],
1569
+ max_local_boundary_speaker_drop=0.15,
1570
+ )
1571
+ assert relaxed.passed
1572
+ assert relaxed.speaker_similarity == primary.candidate_results[0].speaker_similarity
1573
+
1574
+
1575
+ @pytest.mark.parametrize(
1576
+ "indices",
1577
+ [(), (0, 0), (1, 0), (2,), (True,)],
1578
+ )
1579
+ def test_independent_local_semantic_intersection_rejects_unbound_indices(
1580
+ indices,
1581
+ ):
1582
+ primary = verify_trajectory(
1583
+ [
1584
+ CandidateObservation("第一段", "第一段", 1.0),
1585
+ CandidateObservation("第二段", "第二段", 1.0),
1586
+ ],
1587
+ speaker_gate_enabled=False,
1588
+ )
1589
+ independent = verify_trajectory(
1590
+ [CandidateObservation("第一段", "第一段", 1.0)],
1591
+ speaker_gate_enabled=False,
1592
+ )
1593
+
1594
+ with pytest.raises(ValueError, match="primary indices"):
1595
+ intersect_local_semantic_verification(
1596
+ primary,
1597
+ independent,
1598
+ indices,
1599
+ )
1600
+
1601
+
1602
+ def test_local_coverage_eligibility_matches_the_frozen_boundary_proxy():
1603
+ target = "網路片段內容完整"
1604
+ strict = verify_candidate(
1605
+ CandidateObservation(target, target, 1.0),
1606
+ speaker_gate_enabled=False,
1607
+ )
1608
+ boundary = verify_candidate(
1609
+ CandidateObservation(
1610
+ target,
1611
+ target,
1612
+ 2.0,
1613
+ speaker_similarity=0.70,
1614
+ begin_speaker_similarity=0.80,
1615
+ end_speaker_similarity=0.68,
1616
+ )
1617
+ )
1618
+ semantic = verify_candidate(
1619
+ CandidateObservation(target, target[:-1], 1.0),
1620
+ speaker_gate_enabled=False,
1621
+ )
1622
+
1623
+ assert local_candidate_has_coverage_eligibility(
1624
+ strict,
1625
+ max_local_boundary_speaker_drop=0.15,
1626
+ )
1627
+ assert local_candidate_has_coverage_eligibility(
1628
+ boundary,
1629
+ max_local_boundary_speaker_drop=(
1630
+ SEQUENCE_FALLBACK_MAX_LOCAL_BOUNDARY_SPEAKER_DROP
1631
+ ),
1632
+ )
1633
+ assert not local_candidate_has_coverage_eligibility(
1634
+ boundary,
1635
+ max_local_boundary_speaker_drop=0.10,
1636
+ )
1637
+ assert not local_candidate_has_coverage_eligibility(
1638
+ semantic,
1639
+ max_local_boundary_speaker_drop=0.15,
1640
+ )
1641
+
1642
+
1643
+ def test_canonical_attempt_evidence_distinguishes_local_v3_pass_and_not_run():
1644
+ chunks = ("普通內容完整", "網路內容完整")
1645
+ primary = verify_trajectory(
1646
+ [
1647
+ CandidateObservation(chunks[0], chunks[0], 1.0),
1648
+ CandidateObservation(chunks[1], chunks[1], 1.0),
1649
+ ],
1650
+ speaker_gate_enabled=False,
1651
+ )
1652
+ bounded_result = candidate_gate_evidence(primary.candidate_results[1])
1653
+ local_independent = (
1654
+ LocalIndependentGateEvidence(False, None, 0, None),
1655
+ LocalIndependentGateEvidence(True, True, 1, bounded_result),
1656
+ )
1657
+
1658
+ result = run_adaptive_cascade(
1659
+ chunks,
1660
+ 900,
1661
+ lambda candidate_chunks, seed: tuple(candidate_chunks),
1662
+ lambda trajectory, candidate_chunks, seed: CandidateVerification(
1663
+ primary,
1664
+ independent_local_results=local_independent,
1665
+ ),
1666
+ max_candidates=1,
1667
+ )
1668
+ payload = json.loads(
1669
+ format_cascade_evidence_log(
1670
+ result.diagnostics,
1671
+ outcome="returned",
1672
+ generated_chunk_limit=32,
1673
+ selection=result,
1674
+ ).removeprefix(CASCADE_EVIDENCE_LOG_PREFIX)
1675
+ )
1676
+ attempt = payload["attempts"][0]
1677
+
1678
+ assert payload["schema_version"] == 4
1679
+ assert attempt["independent_local_evidence_complete"] is True
1680
+ assert attempt["independent_local_results"] == [
1681
+ {
1682
+ "attempted": False,
1683
+ "passed": None,
1684
+ "proof_count": 0,
1685
+ "result": None,
1686
+ },
1687
+ {
1688
+ "attempted": True,
1689
+ "passed": True,
1690
+ "proof_count": 1,
1691
+ "result": attempt["local_results"][1],
1692
+ },
1693
+ ]
1694
+
1695
+
1696
+ @pytest.mark.parametrize(
1697
+ "bad_evidence",
1698
+ [
1699
+ (LocalIndependentGateEvidence(False, None, 1.0, None),),
1700
+ (LocalIndependentGateEvidence(False, None, 33, None),),
1701
+ (LocalIndependentGateEvidence(True, None, 1, None),),
1702
+ ],
1703
+ )
1704
+ def test_candidate_verification_rejects_malformed_local_v3_attestation(
1705
+ bad_evidence,
1706
+ ):
1707
+ verification = verify_trajectory(
1708
+ [CandidateObservation("內容", "內容", 1.0)],
1709
+ speaker_gate_enabled=False,
1710
+ )
1711
+
1712
+ with pytest.raises(TypeError, match="independent-local"):
1713
+ run_adaptive_cascade(
1714
+ ("內容",),
1715
+ 901,
1716
+ lambda chunks, seed: tuple(chunks),
1717
+ lambda trajectory, chunks, seed: CandidateVerification(
1718
+ verification,
1719
+ independent_local_results=bad_evidence,
1720
+ ),
1721
+ max_candidates=1,
1722
+ )
1723
+
1724
+
1725
  def test_cascade_evidence_success_is_content_free_canonical_and_behavior_golden():
1726
  private_target = "PRIVATE_TARGET_ALPHA"
1727
  private_transcript = "PRIVATE_TRANSCRIPT_OMEGA"
 
3025
  assert limit * text_units <= 800
3026
 
3027
 
3028
+ def test_network_endpoint_units_do_not_change_k32_or_800_work_budget():
3029
+ text = " coastwatch 點 example 點 T W 斜線 tide。"
3030
+ public_units = count_speech_units(text)
3031
+ endpoint_units = count_network_endpoint_duration_units(text)
3032
+
3033
+ assert (public_units, endpoint_units) == (12, 17)
3034
+ assert candidate_limit_for_chunk_budget(
3035
+ 1,
3036
+ total_text_units=public_units,
3037
+ max_generated_text_units=800,
3038
+ ) == 32
3039
+ assert candidate_limit_for_chunk_budget(
3040
+ 1,
3041
+ total_text_units=endpoint_units,
3042
+ max_generated_text_units=800,
3043
+ ) == 32
3044
+ evidence = CandidateGenerationEvidence(
3045
+ chunk_indices=(0,),
3046
+ chunk_text_units=(public_units,),
3047
+ scheduled_cfg=3.0,
3048
+ effective_cfgs=(3.0,),
3049
+ floor_reasons=((),),
3050
+ )
3051
+ result = run_coverage_adaptive_cascade(
3052
+ (text,),
3053
+ 902,
3054
+ lambda chunks, seed: tuple(chunks),
3055
+ lambda trajectory, chunks, seed: verify_trajectory(
3056
+ [CandidateObservation(text, text, 1.0)],
3057
+ chunk_artifacts=[ChunkCandidateArtifact(rms_db=-20.0)],
3058
+ speaker_gate_enabled=False,
3059
+ ),
3060
+ lambda trajectory, chunks, seed: pytest.fail("no refill expected"),
3061
+ sequence_final_verifier=lambda result, chunks: pytest.fail(
3062
+ "no sequence expected"
3063
+ ),
3064
+ generation_evidence_factory=(
3065
+ lambda candidate_index, seed, chunk_indices, chunks: evidence
3066
+ ),
3067
+ max_generated_text_units=800,
3068
+ )
3069
+ assert result.generated_text_units == public_units
3070
+ assert result.diagnostics.attempts[0].chunk_text_units == (public_units,)
3071
+
3072
+
3073
  def test_candidate_limit_requires_complete_positive_text_unit_budget():
3074
  with pytest.raises(ValueError, match="provided together"):
3075
  candidate_limit_for_chunk_budget(1, total_text_units=40)
tests/test_release_pins.py CHANGED
@@ -3,6 +3,8 @@ import hashlib
3
  from pathlib import Path
4
  from types import SimpleNamespace
5
 
 
 
6
 
7
  ROOT = Path(__file__).resolve().parents[1]
8
 
@@ -409,6 +411,232 @@ def test_app_rejects_silent_text_and_coalesces_before_runtime_budgeting():
409
  )
410
 
411
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
  def test_app_emits_one_canonical_content_free_evidence_line_per_terminal_outcome():
413
  source = (ROOT / "app.py").read_text(encoding="utf-8")
414
  tree = ast.parse(source)
@@ -441,7 +669,10 @@ def test_app_emits_one_canonical_content_free_evidence_line_per_terminal_outcome
441
  assert "error.diagnostics" in synthesize_source
442
  assert "cascade.diagnostics" in synthesize_source
443
  assert "final_output=final_evidence" in synthesize_source
444
- assert "CandidateVerification(local_verification)" in qualifier_source
 
 
 
445
  assert "joined_evidence = trajectory_gate_evidence(joined_verification)" in (
446
  qualifier_source
447
  )
@@ -689,7 +920,7 @@ def test_whole_candidate_qualification_uses_the_exact_return_assembler_after_loc
689
  assert "require_verified_final_output(final_verification)" in synthesize_source
690
 
691
 
692
- def test_space_hard_intersects_dual_asr_only_on_exact_whole_waveforms():
693
  source = (ROOT / "app.py").read_text(encoding="utf-8")
694
  tree = ast.parse(source)
695
  functions = {
@@ -714,6 +945,10 @@ def test_space_hard_intersects_dual_asr_only_on_exact_whole_waveforms():
714
  source,
715
  functions["_verify_refill_candidate_trajectory_audio"],
716
  )
 
 
 
 
717
  synthesize_source = ast.get_source_segment(source, functions["_synthesize"])
718
 
719
  assert all(
@@ -724,6 +959,7 @@ def test_space_hard_intersects_dual_asr_only_on_exact_whole_waveforms():
724
  qualify_source,
725
  sequence_source,
726
  refill_source,
 
727
  synthesize_source,
728
  )
729
  )
@@ -733,8 +969,12 @@ def test_space_hard_intersects_dual_asr_only_on_exact_whole_waveforms():
733
  assert "semantic_only=True" in independent_source
734
  assert "cache.verify(" in independent_source
735
  assert "VERIFICATION_ASR_PROFILE" in independent_source
736
- assert "return _verify_trajectory_audio(" in refill_source
737
  assert "_verify_independent_whole_audio" not in refill_source
 
 
 
 
738
 
739
  local_index = qualify_source.index("local_verification = _verify_trajectory_audio(")
740
  local_fail_index = qualify_source.index("if not local_verification.passed:")
 
3
  from pathlib import Path
4
  from types import SimpleNamespace
5
 
6
+ from quality_runtime import LocalIndependentGateEvidence
7
+
8
 
9
  ROOT = Path(__file__).resolve().parents[1]
10
 
 
411
  )
412
 
413
 
414
+ def test_network_endpoint_headroom_is_isolated_from_public_unit_contracts():
415
+ app_source = (ROOT / "app.py").read_text(encoding="utf-8")
416
+ production_source = (ROOT / "production.py").read_text(encoding="utf-8")
417
+ app_tree = ast.parse(app_source)
418
+ production_tree = ast.parse(production_source)
419
+ app_functions = {
420
+ node.name: node
421
+ for node in app_tree.body
422
+ if isinstance(node, ast.FunctionDef)
423
+ }
424
+ production_functions = {
425
+ node.name: node
426
+ for node in production_tree.body
427
+ if isinstance(node, ast.FunctionDef)
428
+ }
429
+ generate_source = ast.get_source_segment(
430
+ app_source,
431
+ app_functions["_generate_chunk"],
432
+ )
433
+ public_counter_source = ast.get_source_segment(
434
+ production_source,
435
+ production_functions["count_speech_units"],
436
+ )
437
+ network_counter_source = ast.get_source_segment(
438
+ production_source,
439
+ production_functions["count_network_endpoint_duration_units"],
440
+ )
441
+
442
+ assert generate_source is not None
443
+ assert public_counter_source is not None
444
+ assert network_counter_source is not None
445
+ assert "count_network_endpoint_duration_units(text)" in generate_source
446
+ assert "if network_conditioned" in generate_source
447
+ assert "duration_units=endpoint_duration_units" in generate_source
448
+ assert "min_len = 2" in generate_source
449
+ assert '"max_len": hard_stop_steps' in generate_source
450
+ assert "expected_steps=expected_steps" in generate_source
451
+ assert "hard_stop_steps=hard_stop_steps" in generate_source
452
+ assert "duration_counter=" in generate_source
453
+ assert "divisor = 2 if token.isdigit() else 4" in public_counter_source
454
+ assert "math.ceil(ascii_run_length / 2)" in network_counter_source
455
+ assert production_source.count(
456
+ "count_network_endpoint_duration_units("
457
+ ) == 1
458
+
459
+
460
+ def test_network_local_dual_asr_capability_is_range_bound_for_initial_and_refill():
461
+ app_source = (ROOT / "app.py").read_text(encoding="utf-8")
462
+ quality_source = (ROOT / "quality_runtime.py").read_text(encoding="utf-8")
463
+ app_tree = ast.parse(app_source)
464
+ quality_tree = ast.parse(quality_source)
465
+ app_functions = {
466
+ node.name: node
467
+ for node in app_tree.body
468
+ if isinstance(node, ast.FunctionDef)
469
+ }
470
+ quality_functions = {
471
+ node.name: node
472
+ for node in quality_tree.body
473
+ if isinstance(node, ast.FunctionDef)
474
+ }
475
+ helper_source = ast.get_source_segment(
476
+ app_source,
477
+ app_functions["_verify_network_local_asr_intersection"],
478
+ )
479
+ initial_source = ast.get_source_segment(
480
+ app_source,
481
+ app_functions["_qualify_candidate_trajectory_audio"],
482
+ )
483
+ refill_source = ast.get_source_segment(
484
+ app_source,
485
+ app_functions["_verify_refill_candidate_trajectory_audio"],
486
+ )
487
+ intersection_source = ast.get_source_segment(
488
+ quality_source,
489
+ quality_functions["intersect_local_semantic_verification"],
490
+ )
491
+
492
+ assert all(
493
+ source is not None
494
+ for source in (
495
+ helper_source,
496
+ initial_source,
497
+ refill_source,
498
+ intersection_source,
499
+ )
500
+ )
501
+ assert "proof_rows[index] for index in selected_indices" in helper_source
502
+ assert "transcriber=transcribe_verification_whisper" in helper_source
503
+ assert "semantic_only=True" in helper_source
504
+ assert "network_fragment_proofs=independent_proof_rows" in helper_source
505
+ assert "intersect_local_semantic_verification(" in helper_source
506
+ assert "[BlueMagpie] network local independent " in helper_source
507
+ assert "proof_count=" in helper_source
508
+ assert "transcript_text" not in helper_source
509
+ for caller_source in (initial_source, refill_source):
510
+ turbo_index = caller_source.index(
511
+ "local_verification = _verify_trajectory_audio("
512
+ )
513
+ intersection_index = caller_source.index(
514
+ "_verify_network_local_asr_intersection("
515
+ )
516
+ assert turbo_index < intersection_index
517
+ assert "proof_rows = _network_fragment_proof_rows(" in caller_source
518
+ assert "network_fragment_proofs=proof_rows" in caller_source
519
+ assert "proof_rows," in caller_source[intersection_index:]
520
+ assert "candidate_seed=" in caller_source[intersection_index:]
521
+ assert "semantic_reasons = [\"semantic_gate\"]" in intersection_source
522
+ assert "\"network_protected_span_mismatch\"" in intersection_source
523
+ assert "chunk_artifacts=primary_verification.chunk_artifacts" in (
524
+ intersection_source
525
+ )
526
+ assert "CASCADE_EVIDENCE_SCHEMA_VERSION = 4" in quality_source
527
+ assert "local_candidate_has_coverage_eligibility(" in helper_source
528
+ assert "independent_local_results=" in initial_source
529
+ assert "independent_local_results=" in refill_source
530
+ assert '"independent_local_evidence_complete"' in quality_source
531
+ assert '"independent_local_results"' in quality_source
532
+
533
+
534
+ def test_network_local_dual_asr_runtime_reuses_exact_proof_rows_and_logs_no_text(
535
+ capsys,
536
+ ):
537
+ source = (ROOT / "app.py").read_text(encoding="utf-8")
538
+ tree = ast.parse(source)
539
+ function = next(
540
+ node
541
+ for node in tree.body
542
+ if (
543
+ isinstance(node, ast.FunctionDef)
544
+ and node.name == "_verify_network_local_asr_intersection"
545
+ )
546
+ )
547
+ module = ast.Module(
548
+ body=[
549
+ ast.ImportFrom(
550
+ module="__future__",
551
+ names=[ast.alias(name="annotations")],
552
+ level=0,
553
+ ),
554
+ function,
555
+ ],
556
+ type_ignores=[],
557
+ )
558
+ ast.fix_missing_locations(module)
559
+
560
+ calls = []
561
+ proof = object()
562
+ skipped_proof = object()
563
+ proof_rows = ((), (proof,), (skipped_proof,))
564
+ primary_results = (object(), object(), object())
565
+ turbo = SimpleNamespace(candidate_results=primary_results)
566
+ independent_result = SimpleNamespace(
567
+ passed=True,
568
+ rejection_reasons=(),
569
+ comparison=SimpleNamespace(
570
+ cer=0.0,
571
+ prefix_cer=0.0,
572
+ suffix_cer=0.0,
573
+ extra_tail_units=0,
574
+ ),
575
+ )
576
+ independent = SimpleNamespace(candidate_results=(independent_result,))
577
+ verification_transcriber = object()
578
+
579
+ def fake_verify(*args, **kwargs):
580
+ calls.append(("verify", args, kwargs))
581
+ return independent
582
+
583
+ def fake_intersect(*args):
584
+ calls.append(("intersect", args))
585
+ return "combined"
586
+
587
+ namespace = {
588
+ "_verify_trajectory_audio": fake_verify,
589
+ "QUALITY_FINAL_ASR_MAX_NEW_TOKENS": 440,
590
+ "SEQUENCE_FALLBACK_MAX_LOCAL_BOUNDARY_SPEAKER_DROP": 0.15,
591
+ "transcribe_verification_whisper": verification_transcriber,
592
+ "intersect_local_semantic_verification": fake_intersect,
593
+ "local_candidate_has_coverage_eligibility": (
594
+ lambda result, **_kwargs: result is primary_results[1]
595
+ ),
596
+ "LocalIndependentGateEvidence": LocalIndependentGateEvidence,
597
+ "candidate_gate_evidence": lambda result: ("bounded", result),
598
+ }
599
+ exec(compile(module, "<isolated-network-local>", "exec"), namespace)
600
+ result = namespace["_verify_network_local_asr_intersection"](
601
+ turbo,
602
+ ("ordinary-audio", "network-audio", "ordinary-audio-2"),
603
+ ("PRIVATE_ORDINARY_A", "PRIVATE_NETWORK_TEXT", "PRIVATE_ORDINARY_B"),
604
+ "anchor",
605
+ proof_rows,
606
+ candidate_seed=123,
607
+ )
608
+
609
+ assert result[0] == "combined"
610
+ verify_call = calls[0]
611
+ assert verify_call[0] == "verify"
612
+ assert verify_call[1][:4] == (
613
+ ("network-audio",),
614
+ ("PRIVATE_NETWORK_TEXT",),
615
+ "anchor",
616
+ 1.0,
617
+ )
618
+ assert verify_call[1][4] == 440
619
+ assert verify_call[2]["transcriber"] is verification_transcriber
620
+ assert verify_call[2]["semantic_only"] is True
621
+ selected_proofs = verify_call[2]["network_fragment_proofs"]
622
+ assert selected_proofs == ((proof,),)
623
+ assert selected_proofs[0] is proof_rows[1]
624
+ assert calls[1] == (
625
+ "intersect",
626
+ (turbo, independent, (1,)),
627
+ )
628
+ evidence = result[1]
629
+ assert evidence[0] == LocalIndependentGateEvidence(False, None, 0, None)
630
+ assert evidence[1].attempted is True
631
+ assert evidence[1].passed is True
632
+ assert evidence[1].proof_count == 1
633
+ assert evidence[1].result == ("bounded", independent_result)
634
+ assert evidence[2] == LocalIndependentGateEvidence(False, None, 1, None)
635
+ log = capsys.readouterr().out
636
+ assert "seed=123 local_chunk_index=1 proof_count=1 passed=True" in log
637
+ assert "PRIVATE_" not in log
638
+
639
+
640
  def test_app_emits_one_canonical_content_free_evidence_line_per_terminal_outcome():
641
  source = (ROOT / "app.py").read_text(encoding="utf-8")
642
  tree = ast.parse(source)
 
669
  assert "error.diagnostics" in synthesize_source
670
  assert "cascade.diagnostics" in synthesize_source
671
  assert "final_output=final_evidence" in synthesize_source
672
+ assert "CandidateVerification(" in qualifier_source
673
+ assert "independent_local_results=independent_local_results" in (
674
+ qualifier_source
675
+ )
676
  assert "joined_evidence = trajectory_gate_evidence(joined_verification)" in (
677
  qualifier_source
678
  )
 
920
  assert "require_verified_final_output(final_verification)" in synthesize_source
921
 
922
 
923
+ def test_space_hard_intersects_dual_asr_on_whole_and_proven_network_locals():
924
  source = (ROOT / "app.py").read_text(encoding="utf-8")
925
  tree = ast.parse(source)
926
  functions = {
 
945
  source,
946
  functions["_verify_refill_candidate_trajectory_audio"],
947
  )
948
+ network_local_source = ast.get_source_segment(
949
+ source,
950
+ functions["_verify_network_local_asr_intersection"],
951
+ )
952
  synthesize_source = ast.get_source_segment(source, functions["_synthesize"])
953
 
954
  assert all(
 
959
  qualify_source,
960
  sequence_source,
961
  refill_source,
962
+ network_local_source,
963
  synthesize_source,
964
  )
965
  )
 
969
  assert "semantic_only=True" in independent_source
970
  assert "cache.verify(" in independent_source
971
  assert "VERIFICATION_ASR_PROFILE" in independent_source
972
+ assert "local_verification = _verify_trajectory_audio(" in refill_source
973
  assert "_verify_independent_whole_audio" not in refill_source
974
+ assert "transcriber=transcribe_verification_whisper" in network_local_source
975
+ assert "semantic_only=True" in network_local_source
976
+ assert "network_fragment_proofs=independent_proof_rows" in network_local_source
977
+ assert "intersect_local_semantic_verification(" in network_local_source
978
 
979
  local_index = qualify_source.index("local_verification = _verify_trajectory_audio(")
980
  local_fail_index = qualify_source.index("if not local_verification.passed:")