105 lines
5.5 KiB
PHP
105 lines
5.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace app\adminapi\service;
|
|
|
|
use app\common\model\tcm\CallRecord;
|
|
use DomainException;
|
|
use think\facade\Db;
|
|
|
|
/** Persist a locally recovered transcript without reopening the realtime session. */
|
|
final class LocalAudioTranscriptRecovery
|
|
{
|
|
public static function save(array $params): array
|
|
{
|
|
foreach (['diagnosis_id', 'call_record_id', 'admin_id'] as $key) {
|
|
$value = $params[$key] ?? null;
|
|
if ((!is_int($value) && !is_string($value))
|
|
|| filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]) === false) {
|
|
throw new DomainException('录音补转写身份参数无效');
|
|
}
|
|
}
|
|
foreach (['recovery_id', 'expected_session_id', 'expected_status', 'audio_url', 'transcript_text'] as $key) {
|
|
if (!isset($params[$key]) || !is_string($params[$key])) {
|
|
throw new DomainException('录音补转写参数无效');
|
|
}
|
|
}
|
|
if (!preg_match('/\Aaudio-[a-f0-9]{32}\z/D', $params['recovery_id'])
|
|
|| mb_strlen($params['expected_session_id']) > 128
|
|
|| !in_array($params['expected_status'], ['', 'pending', 'running', 'partial', 'failed', 'completed'], true)
|
|
|| trim($params['audio_url']) === '' || strlen($params['audio_url']) > 8192) {
|
|
throw new DomainException('录音补转写会话参数无效');
|
|
}
|
|
$text = trim($params['transcript_text']);
|
|
if ($text === '' || mb_strlen($params['transcript_text']) > 100000) {
|
|
throw new DomainException('录音补转写文字必须为1到100000个字符');
|
|
}
|
|
|
|
return Db::transaction(static function () use ($params, $text): array {
|
|
// The realtime upsert/finish endpoints lock the same row, so a late
|
|
// old-session write cannot run after this session switch succeeds.
|
|
$record = CallRecord::where('id', (int) $params['call_record_id'])
|
|
->where('diagnosis_id', (int) $params['diagnosis_id'])
|
|
->where('caller_id', (int) $params['admin_id'])
|
|
->where('caller_type', 'doctor')->lock(true)->find();
|
|
if (!$record) { throw new DomainException('通话记录不存在或无权操作'); }
|
|
if ((int) $record['status'] !== 2) { throw new DomainException('通话结束后才能补转写'); }
|
|
$urls = json_decode((string) ($record['local_audio_urls'] ?? ''), true);
|
|
if ((int) ($record['local_audio_status'] ?? 0) !== 2 || !is_array($urls)
|
|
|| !in_array($params['audio_url'], $urls, true)) {
|
|
throw new DomainException('录音尚未上传或不属于当前通话');
|
|
}
|
|
$result = ['call_record_id' => (int) $record['id'], 'transcription_status' => 'completed'];
|
|
$session = (string) ($record['transcription_session_id'] ?? '');
|
|
$status = (string) ($record['transcription_status'] ?? '');
|
|
if ($status === 'completed' && trim((string) ($record['transcript_text'] ?? '')) !== '') {
|
|
// Lost response retries succeed even with the original CAS values;
|
|
// a different completed session always wins without being changed.
|
|
return $session === $params['recovery_id']
|
|
? $result + ['saved' => true]
|
|
: $result + ['saved' => false, 'already_complete' => true];
|
|
}
|
|
if ($session !== $params['expected_session_id'] || $status !== $params['expected_status']) {
|
|
throw new DomainException('通话转写状态已变化,请刷新后重试');
|
|
}
|
|
|
|
$now = time();
|
|
$segments = [];
|
|
$lines = [];
|
|
// Recognition supplied no diarization or alignment. Do not invent
|
|
// doctor/patient attribution or timestamps; preserve source order by id.
|
|
for ($offset = 0, $index = 0, $length = mb_strlen($text); $offset < $length; $offset += 4000, $index++) {
|
|
$part = mb_substr($text, $offset, 4000);
|
|
$segments[] = [
|
|
'call_record_id' => (int) $record['id'],
|
|
'transcription_session_id' => $params['recovery_id'],
|
|
'segment_id' => $params['recovery_id'] . '-' . $index,
|
|
'speaker_user_id' => '', 'speaker_role' => 'unknown', 'timestamp_ms' => 0,
|
|
'text' => $part, 'create_time' => $now, 'update_time' => $now,
|
|
];
|
|
$lines[] = '未知说话人:' . $part;
|
|
}
|
|
// Keep all old-session segments for audit; the current session is the
|
|
// only one consumed by transcript readers and clinical context builders.
|
|
Db::name('tcm_call_transcript_segment')->insertAll($segments);
|
|
$record->save([
|
|
'transcription_session_id' => $params['recovery_id'],
|
|
'transcription_status' => 'completed',
|
|
'transcription_segment_count' => count($segments),
|
|
'transcript_text' => implode("\n", $lines),
|
|
'transcription_started_at' => $now, 'transcription_finished_at' => $now,
|
|
'update_time' => $now,
|
|
]);
|
|
return $result + ['saved' => true];
|
|
});
|
|
}
|
|
|
|
public static function completionStatus(string $requested, int $actual, int $expected, string $text): string
|
|
{
|
|
if ($requested !== 'completed') { return $requested; }
|
|
if ($actual === 0 || trim($text) === '') { return 'failed'; }
|
|
return $actual < $expected ? 'partial' : 'completed';
|
|
}
|
|
}
|