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 = `
点击“开始实时会议”后,这里会像 demo 一样滚动显示气泡和实时变化。
`; return; } - const lastItem = items[items.length - 1]; - const pendingItem = lastItem?.partial ? lastItem : null; - const baseItems = pendingItem ? items.slice(0, -1) : items; - - if (!baseItems.length && pendingItem) { - const identity = (pendingItem.speaker || pendingItem.label || "进行中").trim() || "进行中"; - const order = getLiveSpeakerOrder(pendingItem.speakerKey || identity); - const side = order % 2 === 0 ? "speaker-left" : "speaker-right"; - $("liveTranscript").innerHTML = ` -
-
-
- - ${escapeHtml(identity)} -
-
${escapeHtml(pendingItem.text || "")}
-
-
- `; - $("liveTranscript").scrollTop = $("liveTranscript").scrollHeight; - return; - } - - const rendered = baseItems.map((item) => { + const rendered = items.map((item) => { const speaker = (item.speaker || "").trim(); - const label = (item.label || "").trim(); - const identity = item.speakerPending ? "未确认说话人" : (speaker || label || "系统"); - const order = getLiveSpeakerOrder(item.speakerKey || identity); + const speakerPending = item.speakerPending || !speaker; + const timeRange = formatLiveTimeRange(item.startTimeMs, item.endTimeMs); + const bodyText = `${escapeHtml(item.text || "")}${item.partial ? " ..." : ""}`; + if (speakerPending) { + return ` +
+
+
+ + 说话人不确定 + ${escapeHtml(timeRange)} +
+
${bodyText}
+
+
+ `; + } + + const order = getLiveSpeakerOrder(item.speakerKey || speaker); const side = order % 2 === 0 ? "speaker-left" : "speaker-right"; const badge = `speaker-color-${order}`; - const headerMain = item.speakerPending ? "未确认说话人" : (speaker || label || "系统"); - const headerMeta = item.speakerPending - ? "待后续句子确认归属" - : (speaker && label && label !== speaker ? label : ""); - const rowClass = `${side}${item.partial || item.speakerPending ? " speaker-unknown" : ""}`; return ` -
+
- ${escapeHtml(headerMain)} - ${headerMeta ? `${escapeHtml(headerMeta)}` : ""} + ${escapeHtml(speaker)} + ${escapeHtml(timeRange)}
-
${escapeHtml(item.text || "")}
+
${bodyText}
`; }); - if (pendingItem) { - const identity = (pendingItem.speaker || pendingItem.label || "进行中").trim() || "进行中"; - const order = getLiveSpeakerOrder(pendingItem.speakerKey || identity); - const side = order % 2 === 0 ? "speaker-left" : "speaker-right"; - rendered.push(` -
-
-
- - ${escapeHtml(identity)} -
-
${escapeHtml(pendingItem.text || "")}
-
-
- `); - } - $("liveTranscript").innerHTML = rendered.join(""); $("liveTranscript").scrollTop = $("liveTranscript").scrollHeight; } @@ -4204,6 +4206,8 @@ ${transcript} speakerPending = false, segmentIndex = null, speakerKey = "", + startTimeMs = 0, + endTimeMs = 0, ) { const safeText = (text || "").trim(); if (!safeText) return; @@ -4216,6 +4220,8 @@ ${transcript} last.speakerPending = speakerPending; last.segmentIndex = segmentIndex; last.speakerKey = speakerKey || last.speakerKey || (segmentIndex === null ? "unknown" : `segment-${segmentIndex}`); + last.startTimeMs = startTimeMs; + last.endTimeMs = endTimeMs; renderLiveTranscript(); return; } @@ -4235,6 +4241,8 @@ ${transcript} existing.speaker = speaker; existing.speakerPending = speakerPending; existing.speakerKey = speakerKey || existing.speakerKey || `segment-${segmentIndex}`; + existing.startTimeMs = startTimeMs; + existing.endTimeMs = endTimeMs; renderLiveTranscript(); return; } @@ -4248,6 +4256,8 @@ ${transcript} speakerPending, segmentIndex, speakerKey: speakerKey || (segmentIndex === null ? "unknown" : `segment-${segmentIndex}`), + startTimeMs, + endTimeMs, }); renderLiveTranscript(); }