'blood', 'values' => ['systolic_pressure' => 120], 'record_date' => null, 'record_time' => '07:45', 'date_text' => '', 'time_text' => '七点四十五分', 'time_estimated' => false, 'evidence' => [['text' => $quote ?? $transcript]]], $extra); $actual = Policy::normalizeExtraction(['transcript' => $transcript, 'summary' => '合成测试', 'items' => [$item]], $recordedAt); $observed = $actual['items'][0] ?? null; $pass = $observed !== null && $observed['record_date'] === $expectedDate && $observed['needs_review'] === $expectedReview; $cases[] = ['id' => $id, 'input' => ['transcript' => $transcript, 'item' => $item, 'recorded_at' => $recordedAt], 'expected' => ['record_date' => $expectedDate, 'needs_review' => $expectedReview], 'actual' => $actual, 'passed' => $pass]; if (!$pass) { $failures[] = $id; } return $actual; }; $yesterday = '昨天七点四十五分收缩压一百二十。'; $run('omitted_date_text_cannot_hide_wrong_explicit', $yesterday, ['record_date' => '2026-02-27'], null, true); $run('wrong_date_text_cannot_hide_wrong_explicit', $yesterday, ['date_text' => '前天', 'record_date' => '2026-02-27'], null, true); $run('wrong_date_text_without_explicit_is_unresolved', $yesterday, ['date_text' => '今天'], null, true); $run('legacy_relative_conflict_stays_unresolved', $yesterday, ['date_text' => '昨天', 'record_date' => '2026-03-01'], null, true); $run('omitted_calendar_derived_from_verbatim_yesterday', $yesterday, [], '2026-02-28', false); $run('day_before_yesterday_crosses_year', '前天七点四十五分收缩压一百二十。', [], '2025-12-30', false, '2026-01-01 10:00:00'); $run('yesterday_leap_day', $yesterday, [], '2024-02-29', false, '2024-03-01 10:00:00'); $run('relative_not_at_start', '我昨天七点四十五分收缩压一百二十。', [], '2026-02-28', false); $run('shortened_quote_cannot_omit_relative_prefix', $yesterday, ['record_date' => '2026-02-27'], null, true, '2026-03-01 10:00:00', '收缩压一百二十'); $run('legacy_joined_utterances_cannot_supply_missing_date', 'synthetic yesterday noon synthetic no date', [], null, true, '2026-03-01 10:00:00', 'synthetic no date'); $run('quoted_date_does_not_take_other_event_date', '前天收缩压一百三十。' . $yesterday, [], '2026-02-28', false, '2026-03-01 10:00:00', $yesterday); $run('fuzzy_range_omitted_date_text', '最近几天七点四十五分收缩压一百二十。', ['record_date' => '2026-03-01'], null, true); $range = $run('multiple_days_are_not_one_day', '昨天或前天七点四十五分收缩压一百二十。', ['record_date' => '2026-02-28'], null, true); $pass = str_contains($range['items'][0]['date_text'], '昨天或前天'); $cases[] = ['id' => 'range_original_wording_retained', 'actual' => $range['items'][0]['date_text'], 'passed' => $pass]; if (!$pass) { $failures[] = 'range_original_wording_retained'; } $run('date_absent_never_defaults_to_today', '七点四十五分收缩压一百二十。', [], null, true); $run('unquoted_relative_cannot_ground_date', '七点四十五分收缩压一百二十。', [], null, true, '2026-03-01 10:00:00', $yesterday); $run('explicit_calendar_contradicts_evidence', '2026年2月28日七点四十五分收缩压一百二十。', ['record_date' => '2026-02-27'], null, true); $run('family_quote_stays_manual', '我父亲昨天七点四十五分收缩压一百二十。', [], '2026-02-28', true); $run('question_stays_manual', '昨天七点四十五分收缩压一百二十吗?', [], '2026-02-28', true); $run('negation_stays_manual', '昨天七点四十五分收缩压不是一百二十。', [], '2026-02-28', true); $run('uncertain_numeric_stays_manual', '昨天七点四十五分收缩压大概一百二十左右。', [], '2026-02-28', true); $run('estimated_numeric_stays_manual', '昨天七点四十五分收缩压估计一百二十。', [], '2026-02-28', true); $run('historical_medication_stays_manual', '以前用药甲,现在停用了。', ['kind' => 'diagnosis', 'values' => ['current_medications' => '药甲']], null, true); $run('prior_year_not_current_day', '去年检查过收缩压一百二十。', ['date_text' => '今天', 'record_date' => '2026-03-01'], null, true); $run('bare_year_is_not_specific_day', '2024年检查过收缩压一百二十。', ['date_text' => '', 'record_date' => '2024-03-01'], null, true); $run('weekday_only_cannot_invent_day', '周五七点四十五分收缩压一百二十。', ['date_text' => '周五', 'record_date' => '2024-02-29'], null, true); $run('weekday_conflicts_with_calendar', '2024年2月29日周五七点四十五分收缩压一百二十。', ['record_date' => '2024-02-29'], null, true); $run('weekday_agrees_with_calendar', '2024年2月29日周四七点四十五分收缩压一百二十。', ['record_date' => '2024-02-29'], '2024-02-29', false); $segment = ['id' => 'seg_2', 'start_ms' => 300000, 'end_ms' => 480000, 'text' => $yesterday]; $evidence = ['segment_id' => $segment['id'], 'text' => $yesterday, 'start_ms' => $segment['start_ms'], 'end_ms' => $segment['end_ms'], 'position_type' => 'segment']; $canonical = ['transcript' => $yesterday, 'transcript_segments' => [$segment], 'summary' => '合成片段', 'items' => [['kind' => 'blood', 'values' => ['systolic_pressure' => 120], 'date_text' => '昨天', 'record_time' => '07:45', 'evidence' => [$evidence]]]]; $normalized = Policy::normalizeExtraction($canonical, '2026-03-01 10:00:00'); $pass = ($normalized['transcript_segments'] ?? null) === [$segment] && $normalized['items'][0]['evidence'][0] === $evidence; $cases[] = ['id' => 'immutable_chunk_boundaries_preserved', 'input' => $canonical, 'actual' => $normalized, 'passed' => $pass]; if (!$pass) { $failures[] = 'immutable_chunk_boundaries_preserved'; } $twice = Policy::normalizeExtraction($normalized, '2026-03-01 10:00:00'); $pass = $normalized === $twice; $cases[] = ['id' => 'renormalization_retains_ids_and_metadata', 'actual' => $twice, 'passed' => $pass]; if (!$pass) { $failures[] = 'renormalization_retains_ids_and_metadata'; } foreach ([ 'unknown_segment' => ['segment_id' => 'seg_unknown'], 'invented_word_timestamp' => ['start_ms' => 320000, 'end_ms' => 325000], 'invented_word_alignment_type' => ['position_type' => 'word'], 'missing_boundary' => ['end_ms' => null], 'quote_not_in_cited_segment' => ['text' => '今天原话没有此句'], ] as $id => $bad) { $input = $canonical; $input['items'][0]['evidence'][0] = array_replace($evidence, $bad); $actual = Policy::normalizeExtraction($input, '2026-03-01 10:00:00'); $pass = $actual['items'] === [] && count($actual['uncertainties']) === 1; $cases[] = ['id' => $id, 'input' => $input, 'actual' => $actual, 'passed' => $pass]; if (!$pass) { $failures[] = $id; } } $input = $canonical; $input['transcript'] = $input['transcript_segments'][0]['text'] = $input['items'][0]['evidence'][0]['text'] = '收缩压一百二十。'; $input['items'][0]['record_date'] = '2026-03-01'; $input['items'][0]['date_text'] = '今天'; $actual = Policy::normalizeExtraction($input, '2026-03-01 10:00:00'); $pass = $actual['items'][0]['record_date'] === null && $actual['items'][0]['needs_review']; $cases[] = ['id' => 'strict_pipeline_rejects_ungrounded_model_day', 'input' => $input, 'actual' => $actual, 'passed' => $pass]; if (!$pass) { $failures[] = 'strict_pipeline_rejects_ungrounded_model_day'; } $input['transcript_segments'][] = ['id' => 'seg_3', 'start_ms' => 480000, 'end_ms' => 600000, 'text' => '昨天收缩压一百二十。']; $input['transcript'] .= "\n" . $input['transcript_segments'][1]['text']; $input['items'][0]['record_date'] = null; $input['items'][0]['date_text'] = ''; $actual = Policy::normalizeExtraction($input, '2026-03-01 10:00:00'); $pass = $actual['items'][0]['record_date'] === null && $actual['items'][0]['needs_review']; $cases[] = ['id' => 'repeated_quote_cannot_borrow_date_from_other_segment', 'input' => $input, 'actual' => $actual, 'passed' => $pass]; if (!$pass) { $failures[] = 'repeated_quote_cannot_borrow_date_from_other_segment'; } $withSilence = $canonical; array_unshift($withSilence['transcript_segments'], ['id' => 'silent_chunk', 'start_ms' => 0, 'end_ms' => 10000, 'text' => '']); $actual = null; try { $actual = Policy::normalizeExtraction($withSilence, '2026-03-01 10:00:00'); } catch (DomainException $error) { /* observed rejection belongs in this regression */ } $pass = $actual !== null && count($actual['items']) === 1 && ($actual['transcript_segments'][0]['text'] ?? null) === ''; $cases[] = ['id' => 'silent_asr_chunk_preserved_without_invented_text', 'passed' => $pass]; if (!$pass) { $failures[] = 'silent_asr_chunk_preserved_without_invented_text'; } echo json_encode(['suite' => 'followup-audio-date-evidence', 'synthetic_only' => true, 'cases' => $cases, 'passed' => count($cases) - count($failures), 'total' => count($cases), 'failures' => $failures], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR) . PHP_EOL; exit($failures === [] ? 0 : 1);