调整气泡累积逻辑

main
Bifang 2026-09-30 13:34:57 +08:00
parent b9fa3e4d5c
commit fb710fadff
2 changed files with 39 additions and 19 deletions

View File

@ -2021,14 +2021,23 @@ class Qwen3ASRService:
self, self,
ctx: ConnectionContext, ctx: ConnectionContext,
text: str, text: str,
*,
native_stream_snapshot: bool = False,
) -> str: ) -> str:
"""Expose only newly stabilized prefixes; the final event carries revisions.""" """Publish a growing preview while leaving Qwen's revisable tail for finalization."""
if native_stream_snapshot:
# Native Qwen text is a rolling snapshot. Exposing its unstable tail
# can make the next correction fail the append-only check and freeze
# the bubble at its first preview. Only publish the confirmed prefix.
candidate = ctx.stable_partial_prefix.strip()
else:
candidate = self._partial_display_text(ctx, text) candidate = self._partial_display_text(ctx, text)
previous = ctx.partial_preview_text previous = ctx.partial_preview_text
if not previous or candidate.startswith(previous): if candidate and (not previous or candidate.startswith(previous)):
ctx.partial_preview_text = candidate ctx.partial_preview_text = candidate
# Qwen may rewrite its rolling snapshot. Keep the already shown prefix # If Qwen revises already published text, keep the preview fixed. The
# fixed until finalization so the browser never retracts earlier words. # final segment event will replace it with the model's finalized result.
return ctx.partial_preview_text return ctx.partial_preview_text
def _is_suspicious_segment_text( def _is_suspicious_segment_text(
@ -3150,6 +3159,7 @@ class Qwen3ASRService:
partial_display = self._append_only_partial_preview( partial_display = self._append_only_partial_preview(
ctx, ctx,
visible_observed, visible_observed,
native_stream_snapshot=has_native_stream_snapshot,
) )
partial_display = self._clip_partial_text(partial_display, ctx) partial_display = self._clip_partial_text(partial_display, ctx)
if ( if (

View File

@ -4211,24 +4211,34 @@ ${transcript}
) { ) {
const safeText = (text || "").trim(); const safeText = (text || "").trim();
if (!safeText) return; if (!safeText) return;
if (partial && state.liveTranscript.length) { if (partial) {
const last = state.liveTranscript[state.liveTranscript.length - 1]; const existingPartialIndex = state.liveTranscript.findIndex(
if (last.partial && (segmentIndex === null || last.segmentIndex === segmentIndex)) { (item) => item.partial && (segmentIndex === null || item.segmentIndex === segmentIndex),
last.label = label; );
last.text = safeText; if (existingPartialIndex >= 0) {
last.speaker = speaker; const existingPartial = state.liveTranscript[existingPartialIndex];
last.speakerPending = speakerPending; existingPartial.label = label;
last.segmentIndex = segmentIndex; existingPartial.text = safeText;
last.speakerKey = speakerKey || last.speakerKey || (segmentIndex === null ? "unknown" : `segment-${segmentIndex}`); existingPartial.speaker = speaker;
last.startTimeMs = startTimeMs; existingPartial.speakerPending = speakerPending;
last.endTimeMs = endTimeMs; existingPartial.segmentIndex = segmentIndex;
existingPartial.speakerKey = speakerKey || existingPartial.speakerKey || (segmentIndex === null ? "unknown" : `segment-${segmentIndex}`);
existingPartial.startTimeMs = startTimeMs;
existingPartial.endTimeMs = endTimeMs;
renderLiveTranscript(); renderLiveTranscript();
return; return;
} }
} }
if (!partial && state.liveTranscript.length) { if (!partial) {
const last = state.liveTranscript[state.liveTranscript.length - 1]; // A late speaker update for an older segment must not erase the live preview.
if (last.partial) state.liveTranscript.pop(); const partialIndex = state.liveTranscript.findIndex(
(item) => item.partial && (
segmentIndex === null
? item.segmentIndex === null
: item.segmentIndex === segmentIndex
),
);
if (partialIndex >= 0) state.liveTranscript.splice(partialIndex, 1);
} }
if (!partial && segmentIndex !== null) { if (!partial && segmentIndex !== null) {