feat: add durable ASR-to-patient extraction and guarded review

This commit is contained in:
2026-10-08 15:08:42 +08:00
parent 8bb4bd07ae
commit 13948f5982
34 changed files with 2377 additions and 56 deletions
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace app\common\service\followupaudio;
/** Fail-closed audio transport; explicit Dify/OpenAI driver, never text or provider fallback. */
/** Fail-closed explicit Dify, audio-model or ASR→text-extraction driver. Never implicit provider fallback. */
final class FollowupAudioDify
{
private array $settings;
@@ -27,7 +27,8 @@ final class FollowupAudioDify
if (!FollowupAudioProviderConfig::verified((string) ($task['model_key'] ?? ''), $this->settings, $this->provider)) {
throw new FollowupAudioException('AUDIO_NOT_VERIFIED');
}
if ((int) ($task['upstream_started_at'] ?? 0) > 0) {
$resolved = FollowupAudioProviderConfig::resolve((string) $task['model_key'], $this->settings, $this->provider);
if ($resolved['driver'] !== 'asr_then_llm' && (int) ($task['upstream_started_at'] ?? 0) > 0) {
throw new FollowupAudioException('RECONCILIATION_REQUIRED', true);
}
$upload = FollowupAudioUpload::session((string) ($task['upload_id'] ?? ''));
@@ -64,6 +65,14 @@ final class FollowupAudioDify
private function analyzeFile(string $path, array $task, callable $heartbeat, bool $synthetic = false): array
{
$provider = FollowupAudioProviderConfig::resolve((string) ($task['model_key'] ?? ''), $this->settings, $this->provider);
if ($provider['driver'] === 'asr_then_llm') {
if ($synthetic) {
$task['upstream_ids_json'] = json_encode(['provider_fingerprint' => $provider['fingerprint']], JSON_THROW_ON_ERROR);
}
$checkpoint = !empty($task['pipeline_cipher']) ? FollowupAudioStore::open((int) $task['id'], 'pipeline', $task['pipeline_cipher']) : [];
return (new FollowupAudioPipeline($this->settings, $provider, $this->transport))->run($path, $task, $heartbeat, $checkpoint);
}
[$base, $key, $timeout, $provider] = $this->resolveProfile((string) ($task['model_key'] ?? ''));
if (!str_starts_with($base, 'https://') && (!$synthetic || empty($this->settings['allow_insecure_synthetic']))) {
throw new FollowupAudioException('HTTPS_REQUIRED');