feat: preserve stereo channels and require reviewed speaker roles

This commit is contained in:
2026-10-08 16:15:41 +08:00
parent 13948f5982
commit a20d3565af
28 changed files with 1134 additions and 113 deletions
@@ -77,6 +77,7 @@ final class FollowupAudioPolicy
$seenEvents = [];
$segments = array_key_exists('transcript_segments', $result) ? self::segments($result['transcript_segments']) : null;
$segmentIndex = $segments === null ? [] : array_column($segments, null, 'id');
$channelMode = $segments !== null && array_key_exists('channel', $segments[0]);
foreach ($result['items'] as $index => $raw) {
try {
if (!is_array($raw) || !is_string($raw['kind'] ?? null) || !is_array($raw['values'] ?? null)) {
@@ -93,7 +94,9 @@ final class FollowupAudioPolicy
if ($segment === null || !str_contains($segment['text'], $quote['text'])
|| ($quote['position_type'] ?? '') !== 'segment'
|| ($quote['start_ms'] ?? null) !== $segment['start_ms']
|| ($quote['end_ms'] ?? null) !== $segment['end_ms']) {
|| ($quote['end_ms'] ?? null) !== $segment['end_ms']
|| array_key_exists('channel', $quote) !== array_key_exists('channel', $segment)
|| (array_key_exists('channel', $segment) && $quote['channel'] !== $segment['channel'])) {
throw new DomainException('FOLLOWUP_AUDIO_EVIDENCE_SEGMENT_INVALID');
}
} elseif (isset($quote['segment_id'])) {
@@ -109,7 +112,7 @@ final class FollowupAudioPolicy
// Approved default clocks are estimates, never claims of an exact spoken measurement time.
$estimated = ($time === null && $kind !== 'diagnosis') || !empty($raw['time_estimated']);
if ($time === null && $kind !== 'diagnosis') { $time = self::estimatedTime($period); }
$needsReview = !empty($raw['needs_review']) || !$quoted || $dateReview
$needsReview = $channelMode || !empty($raw['needs_review']) || !$quoted || $dateReview
|| FollowupAudioFields::requiresClinicalReview($kind, $values)
|| self::riskyEvidenceContext($result['transcript'], $evidence)
|| ($kind !== 'diagnosis' && ($date === null || $estimated));
@@ -320,20 +323,8 @@ final class FollowupAudioPolicy
private static function segments($raw): array
{
if (!is_array($raw) || !array_is_list($raw) || $raw === [] || count($raw) > FollowupAudioTranscriptPrompt::MAX_SEGMENTS) {
throw new DomainException('FOLLOWUP_AUDIO_SEGMENTS_INVALID');
}
$seen = [];
foreach ($raw as $segment) {
if (!is_array($segment) || !is_string($segment['id'] ?? null)
|| !preg_match('/^[A-Za-z0-9_-]{1,128}$/D', $segment['id']) || isset($seen[$segment['id']])
|| !is_string($segment['text'] ?? null) || strlen($segment['text']) > 2000000
|| !is_int($segment['start_ms'] ?? null) || !is_int($segment['end_ms'] ?? null)
|| $segment['start_ms'] < 0 || $segment['end_ms'] <= $segment['start_ms'] || $segment['end_ms'] > 3600000) {
throw new DomainException('FOLLOWUP_AUDIO_SEGMENTS_INVALID');
}
$seen[$segment['id']] = true;
}
if (!is_array($raw)) { throw new DomainException('FOLLOWUP_AUDIO_SEGMENTS_INVALID'); }
FollowupAudioTranscriptPrompt::validateSegments($raw);
return $raw;
}
@@ -359,7 +350,7 @@ final class FollowupAudioPolicy
}
$quote['segment_id'] = $entry['segment_id'];
}
$quote['text'] = trim($entry['text']);
$quote['text'] = isset($entry['segment_id']) ? $entry['text'] : trim($entry['text']);
foreach (['start_ms', 'end_ms'] as $key) {
if (isset($entry[$key])) {
if (!is_int($entry[$key]) || $entry[$key] < 0 || $entry[$key] > 3600000) {
@@ -377,6 +368,13 @@ final class FollowupAudioPolicy
}
$quote['position_type'] = 'segment';
}
if (array_key_exists('channel', $entry)) {
if (!is_int($entry['channel']) || !in_array($entry['channel'], [0, 1], true)
|| !isset($quote['segment_id']) || ($quote['position_type'] ?? '') !== 'segment') {
throw new DomainException('FOLLOWUP_AUDIO_EVIDENCE_SEGMENT_INVALID');
}
$quote['channel'] = $entry['channel'];
}
$evidence[] = $quote;
}
return $evidence;