feat: preserve stereo channels and require reviewed speaker roles
This commit is contained in:
@@ -33,29 +33,43 @@ final class FollowupAudioPipeline
|
||||
}
|
||||
self::beat($heartbeat, []);
|
||||
FollowupAudioPolicy::strictRecordedAt((string) ($task['recorded_at'] ?? ''));
|
||||
$chunkMs = (int) $this->provider['chunk_seconds'] * 1000;
|
||||
FollowupAudioPipelineMedia::assertChunkPlan((float) ($task['duration_seconds'] ?? 0), $chunkMs);
|
||||
$media = new FollowupAudioPipelineMedia($this->settings);
|
||||
$audio = $media->inspect($path, (string) ($task['sha256'] ?? ''));
|
||||
if (abs((float) $task['duration_seconds'] - $audio['duration']) > 0.08) { throw new FollowupAudioException('AUDIO_INVALID'); }
|
||||
FollowupAudioPipelineMedia::assertChunkPlan($audio['duration'], $chunkMs);
|
||||
$chunkMs = (int) ($audio['channels'] === 2 ? $this->provider['stereo_chunk_seconds'] : $this->provider['chunk_seconds']) * 1000;
|
||||
FollowupAudioPipelineMedia::assertChunkPlan($audio['duration'], $chunkMs, $audio['channels']);
|
||||
$state = $checkpoint;
|
||||
if ($state === []) {
|
||||
if ((int) ($task['upstream_started_at'] ?? 0) > 0) { throw new FollowupAudioException('RECONCILIATION_REQUIRED', true); }
|
||||
$next = FollowupAudioPipelineCheckpoint::initial($task, $this->provider['fingerprint'], $this->provider['chunk_seconds'] * 1000);
|
||||
$next = FollowupAudioPipelineCheckpoint::initial($task, $this->provider['fingerprint'], $chunkMs, $audio['channels']);
|
||||
$this->save($state, $next, $task, $heartbeat, 'transcribing');
|
||||
} else { FollowupAudioPipelineCheckpoint::validate($state, $task); }
|
||||
} else {
|
||||
FollowupAudioPipelineCheckpoint::validate($state, $task);
|
||||
if ($state['schema_version'] !== 2 || $state['channel_policy'] !== $this->provider['channel_policy']) {
|
||||
throw new FollowupAudioException('PIPELINE_POLICY_CHANGED', true);
|
||||
}
|
||||
if ($state['source_channels'] !== $audio['channels'] || $state['chunk_ms'] !== $chunkMs) { throw new FollowupAudioException('AUDIO_INVALID', true); }
|
||||
}
|
||||
if (FollowupAudioPipelineCheckpoint::hasIntent($state)) { throw new FollowupAudioException('RECONCILIATION_REQUIRED', true); }
|
||||
$directory = sys_get_temp_dir() . '/followup-asr-' . bin2hex(random_bytes(16));
|
||||
if (!mkdir($directory, 0700)) { throw new FollowupAudioException('AUDIO_NORMALIZATION_FAILED'); }
|
||||
$chunkPath = $directory . '/chunk.wav';
|
||||
try {
|
||||
foreach ($state['chunks'] as $index => $chunk) {
|
||||
if ($chunk['state'] === 'complete') { continue; }
|
||||
if (in_array($chunk['state'], ['complete', 'local_silence'], true)) { continue; }
|
||||
self::beat($heartbeat, []);
|
||||
$this->assertSource($audio['path'], $task['sha256']);
|
||||
$media->chunk($audio, $chunk, $chunkPath, $heartbeat);
|
||||
$this->assertSource($audio['path'], $task['sha256']);
|
||||
$silence = FollowupAudioPipelineMedia::digitalSilence($chunkPath);
|
||||
if ($silence !== null) {
|
||||
$next = $state;
|
||||
$next['chunks'][$index] += $silence + ['text' => '', 'source' => 'local_silence'];
|
||||
$next['chunks'][$index]['state'] = 'local_silence';
|
||||
$this->save($state, $next, $task, $heartbeat, 'transcribing');
|
||||
unlink($chunkPath);
|
||||
continue;
|
||||
}
|
||||
$next = $state;
|
||||
$next['chunks'][$index]['state'] = 'intent';
|
||||
$next['chunks'][$index]['request_id'] = 'fa-' . bin2hex(random_bytes(16));
|
||||
@@ -106,8 +120,8 @@ final class FollowupAudioPipeline
|
||||
throw new FollowupAudioException('UPSTREAM_SCHEMA_INVALID');
|
||||
}
|
||||
$result = self::validateAnswer($state['extraction']['answer'], $transcript, $segments, $task['recorded_at']);
|
||||
$result['uncertainties'][] = ($audio['channels'] > 1 ? '多声道已合并为单声道转写;' : '')
|
||||
. '未进行说话人分离,患者、家属和客服的归属须人工回听核对。';
|
||||
$result['uncertainties'][] = ($audio['channels'] === 2 ? '左右声道已独立转写;同一时间窗可能交叠,不代表话轮或角色已识别。' : '单声道转写未进行说话人分离。')
|
||||
. '患者、家属和客服归属须人工回听核对。';
|
||||
return $result;
|
||||
} finally { if (is_file($chunkPath)) { unlink($chunkPath); } rmdir($directory); }
|
||||
}
|
||||
@@ -235,6 +249,7 @@ final class FollowupAudioPipeline
|
||||
|| !str_contains($byId[$evidence['segment_id']]['text'], $evidence['text'])) { throw new FollowupAudioException('UPSTREAM_SCHEMA_INVALID'); }
|
||||
$segment = $byId[$evidence['segment_id']];
|
||||
$evidence += ['start_ms' => $segment['start_ms'], 'end_ms' => $segment['end_ms'], 'position_type' => 'segment'];
|
||||
if (array_key_exists('channel', $segment)) { $evidence['channel'] = $segment['channel']; }
|
||||
}
|
||||
unset($evidence);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user