From b9fa3e4d5c3082fe7ca0ef615b578555afe11184 Mon Sep 17 00:00:00 2001 From: Bifang <915779419@qq.com> Date: Wed, 30 Sep 2026 11:04:37 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=B5=8B=E8=AF=95=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E6=B0=94=E6=B3=A1=E6=B8=B2=E6=9F=93=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/qwen3_websocket_asr.py | 21 ++-- test_web/index.html | 142 +++++++++++++++------------- 2 files changed, 85 insertions(+), 78 deletions(-) diff --git a/app/services/qwen3_websocket_asr.py b/app/services/qwen3_websocket_asr.py index 948e180..776c5e8 100644 --- a/app/services/qwen3_websocket_asr.py +++ b/app/services/qwen3_websocket_asr.py @@ -415,8 +415,8 @@ class Qwen3ASRService: sentence_text: Optional[str] = None, ) -> Dict[str, Any]: speaker_id = self._tencent_speaker_id(payload.get("speaker_id")) - if payload.get("speaker_pending"): - speaker_id = -1 + # Keep a provisional cluster ID visible while later segments refine it. + # A genuinely unresolved segment already carries speaker_id=-1. sentence = { "sentence_id": int(payload.get("index", 0)), "sentence_type": int(sentence_type), @@ -2435,10 +2435,10 @@ class Qwen3ASRService: stable = self._is_stable_speaker_info(speaker_info) speaker_info["speaker_pending"] = not stable cluster_index = speaker_info.get("_cluster_index") + has_candidate_speaker = speaker_info.get("speaker_id") not in (None, "", -1, "-1") if ( - stable - and - not speaker_info.get("registry_speaker_id") + has_candidate_speaker + and not speaker_info.get("registry_speaker_id") and not speaker_info.get("user_id") ): safe_cluster_index = self._safe_int(cluster_index) @@ -2461,10 +2461,6 @@ class Qwen3ASRService: ): ctx.speaker_display_map[safe_cluster_index] = str(speaker_info["speaker_id"]) - if not stable: - speaker_info["speaker_id"] = -1 - speaker_info["speaker_name"] = "" - return speaker_info async def _resolve_and_emit_segment_speaker( @@ -2505,8 +2501,6 @@ class Qwen3ASRService: segment_end_ms=segment_end_ms, speaker_info=speaker_info, ) - if before_view == after_view: - return queue_backlog = ctx.speaker_job_queue.qsize() updates: List[Dict[str, Any]] = [] if queue_backlog <= 1: @@ -2520,7 +2514,10 @@ class Qwen3ASRService: int(updated["segment_index"]), ) return - await self._emit_speaker_update(websocket, ctx, task_id, segment_index) + # Re-clustering may resolve pending IDs even if this segment's + # immediate speaker fields did not change. + if before_view != after_view: + await self._emit_speaker_update(websocket, ctx, task_id, segment_index) except Exception as exc: logger.warning("[%s] Async speaker resolution failed for segment %s: %s", task_id, segment_index, exc) diff --git a/test_web/index.html b/test_web/index.html index c0278cc..4fb91c6 100644 --- a/test_web/index.html +++ b/test_web/index.html @@ -1727,6 +1727,24 @@ margin-bottom: 2px; } + /* Match Asr-demo: alternate speaker lanes and keep unresolved speech neutral. */ + .bubble-row.speaker-left { justify-content: flex-start; } + .bubble-row.speaker-right { justify-content: flex-end; } + .speaker-right .bubble-header { justify-content: flex-end; } + .speaker-right .bubble-body { + border-top-left-radius: 12px; + border-top-right-radius: 4px; + text-align: right; + } + .bubble-row.speaker-unknown .bubble-body { + background: #f0f0f0; + color: #999; + font-style: italic; + border-radius: 12px; + border-top-left-radius: 4px; + } + .speaker-badge.speaker-color-unknown { background-color: #ccc; } + .bubble-wrapper { max-width: min(78%, 760px); } @@ -3920,18 +3938,13 @@ ${transcript} function formatLiveSpeaker(meta) { if (!meta) return ""; - const rawName = meta.speaker_name ?? meta.speaker_id ?? ""; const numericSpeakerId = Number(meta.speaker_id); - if (Number.isFinite(numericSpeakerId) && numericSpeakerId < 0 && !meta.speaker_name) { - return ""; + const name = String(meta.speaker_name || "").trim(); + if (name) return name; + if (Number.isFinite(numericSpeakerId) && numericSpeakerId >= 0) { + return `说话人 ${numericSpeakerId}`; } - const name = String(rawName).trim(); - if (!name) return ""; - const confidence = Number(meta.speaker_confidence); - if (Number.isFinite(confidence) && confidence > 0) { - return `${name} (${confidence.toFixed(2)})`; - } - return name; + return typeof meta.speaker_id === "string" ? meta.speaker_id.trim() : ""; } function buildLiveSpeakerKey(meta, fallbackSegmentIndex = null) { @@ -3940,7 +3953,6 @@ ${transcript} } if (meta.user_id) return `user:${String(meta.user_id).trim()}`; if (meta.registry_speaker_id) return `registry:${String(meta.registry_speaker_id).trim()}`; - if (meta.speaker_name) return `name:${String(meta.speaker_name).trim()}`; const numericSpeakerId = Number(meta.speaker_id); if (Number.isFinite(numericSpeakerId) && numericSpeakerId >= 0) { return `speaker:${numericSpeakerId}`; @@ -3948,9 +3960,20 @@ ${transcript} if (typeof meta.speaker_id === "string" && meta.speaker_id.trim()) { return `speaker:${meta.speaker_id.trim()}`; } + if (meta.speaker_name) return `name:${String(meta.speaker_name).trim()}`; return fallbackSegmentIndex === null ? "unknown" : `segment-${fallbackSegmentIndex}`; } + function formatLiveTimeRange(startMs, endMs) { + const formatTime = (value) => { + const totalSeconds = Math.max(0, Math.floor(Number(value || 0) / 1000)); + const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, "0"); + const seconds = String(totalSeconds % 60).padStart(2, "0"); + return `${minutes}:${seconds}`; + }; + return `${formatTime(startMs)} - ${formatTime(endMs)}`; + } + function getLiveSpeakerOrder(key) { const safeKey = String(key ?? "unknown").trim() || "unknown"; if (!(safeKey in state.liveSpeakerOrderMap)) { @@ -3980,14 +4003,17 @@ ${transcript} const isPartial = Number(sentence?.sentence_type ?? 0) === 0; const speakerLabel = formatLiveSpeaker(sentence); const speakerKey = buildLiveSpeakerKey(sentence, segmentIndex); + const speakerPending = Number(sentence?.speaker_id) < 0 && !speakerLabel; addLiveEntry( `${isPartial ? "进行中" : "已确认"} Segment ${segmentIndex + 1}${speakerLabel ? ` · ${speakerLabel}` : ""}`, (sentence?.sentence || "").trim(), isPartial, speakerLabel, - Number(sentence?.speaker_id) < 0, + speakerPending, segmentIndex, speakerKey, + Number(sentence?.start_time ?? 0), + Number(sentence?.end_time ?? 0), ); }); const confirmedCount = state.liveTranscript.filter((item) => !item.partial).length; @@ -4010,6 +4036,8 @@ ${transcript} text: sentence?.sentence || "", speaker_id: sentence?.speaker_id, speaker_name: sentence?.speaker_name, + start_time: sentence?.start_time, + end_time: sentence?.end_time, })), "", sentences.length ? Number(sentences[sentences.length - 1]?.sentence_id ?? 0) : 0, @@ -4036,14 +4064,17 @@ ${transcript} const segmentIndex = Number(segment?.index ?? idx); const speakerLabel = formatLiveSpeaker(segment); const speakerKey = buildLiveSpeakerKey(segment, segmentIndex); + const speakerPending = Number(segment?.speaker_id) < 0 && !speakerLabel; return { label: `已确认 Segment ${segmentIndex + 1}${speakerLabel ? ` · ${speakerLabel}` : ""}`, text: (segment?.text || "").trim(), partial: false, speaker: speakerLabel, - speakerPending: Number(segment?.speaker_id) < 0, + speakerPending, segmentIndex, speakerKey, + startTimeMs: Number(segment?.start_time ?? 0), + endTimeMs: Number(segment?.end_time ?? 0), }; }).filter((item) => item.text); @@ -4070,72 +4101,43 @@ ${transcript} $("liveTranscript").innerHTML = `