|
4 | 4 | * @ai-context: 用户要求"转写流要简要"——设计原则:单行紧凑卡片、色点区分来源 |
5 | 5 | * (字幕=绿、语音=灰、画面=蓝)、无冗余装饰;列表超限截断(计数保留); |
6 | 6 | * 新内容自动跟随滚动。 |
7 | | - * @ai-context: 2026-08 用户需求:ASR 流式返回显示**所有未沉淀行**(识别中 partial + |
8 | | - * 已定稿待沉淀 committed),不再折叠为单行——连续定稿(快速短句) |
9 | | - * 时各行并存不被覆盖丢失;新句首个 partial 到达时统一沉淀。 |
| 7 | + * @ai-context: 2026-08 用户需求:ASR 流式返回显示**所有未沉淀行**——识别中的 |
| 8 | + * partial 按句读切分为多行灰斜体全部显示(不再一行越滚越长); |
| 9 | + * 已定稿待沉淀 committed 行黑色并存(连续定稿不互相覆盖丢失); |
| 10 | + * 新句首个 partial 到达时统一沉淀入列表。 |
10 | 11 | * @ai-context: 状态机:初始化(模型加载)→ 采集中 → 停止中 → 融合中 → 完成/失败, |
11 | 12 | * 由 live:status / session:fusing/fused/failed 事件推导,父组件控制显隐。 |
12 | 13 | */ |
@@ -64,6 +65,29 @@ function fmtTime(ms: number): string { |
64 | 65 | return `${String(Math.floor(s / 60)).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}`; |
65 | 66 | } |
66 | 67 |
|
| 68 | +/** |
| 69 | + * 中文句读切分(识别中行展示用,2026-08 用户需求:识别中的灰斜体内容全部显示)。 |
| 70 | + * |
| 71 | + * @ai-context: 流式 partial 是整句候选且可能含多个句子(zipformer 中文模型输出 |
| 72 | + * 带句读;asr_clean 的跨标点重复/纯标点幻觉处理即依赖此事实)—— |
| 73 | + * 现状整句挤在一行灰斜里越滚越长;按句读切分后每句一行,全部可见。 |
| 74 | + * 句读保留在句尾;无句读尾段为"残余"(调用方加 … 表示仍在识别)。 |
| 75 | + * 不切英文句点(Mr./U.S. 缩写防误切)与逗号(句内成分不拆行)。 |
| 76 | + */ |
| 77 | +function splitBySentence(text: string): string[] { |
| 78 | + const parts: string[] = []; |
| 79 | + let buf = ""; |
| 80 | + for (const ch of text) { |
| 81 | + buf += ch; |
| 82 | + if ("。!?!?…".includes(ch)) { |
| 83 | + parts.push(buf); |
| 84 | + buf = ""; |
| 85 | + } |
| 86 | + } |
| 87 | + if (buf.trim().length > 0) parts.push(buf); |
| 88 | + return parts; |
| 89 | +} |
| 90 | + |
67 | 91 | export default function LiveActivityPanel({ sessionId }: { sessionId?: number | null }) { |
68 | 92 | const [tab, setTab] = useState<"transcript" | "ocr">("transcript"); |
69 | 93 | // 状态机(简要徽标文本) |
@@ -287,37 +311,73 @@ export default function LiveActivityPanel({ sessionId }: { sessionId?: number | |
287 | 311 | ⋯ 共 {totalTranscript} 段,仅显示最近 {SHOW_TRANSCRIPT_LINES} 条(会话页可看全部) |
288 | 312 | </p> |
289 | 313 | )} |
290 | | - {/* 2026-08 用户需求:ASR 未沉淀行全部展示(识别中灰斜 + 已定稿待沉淀黑), |
291 | | - 连续定稿各行并存;新句首个 partial 到达时统一沉淀入上方列表 */} |
292 | | - {partials.map((p) => ( |
293 | | - <div |
294 | | - key={p.id} |
295 | | - style={{ |
296 | | - display: "flex", |
297 | | - gap: 8, |
298 | | - alignItems: "baseline", |
299 | | - fontSize: 13, |
300 | | - color: p.committed ? "#374151" : "#9ca3af", |
301 | | - fontStyle: p.committed ? "normal" : "italic", |
302 | | - }} |
303 | | - > |
304 | | - <span style={{ fontSize: 11, width: 44, flexShrink: 0, fontVariantNumeric: "tabular-nums" }}> |
305 | | - {p.committed ? fmtTime(p.time) : fmtTime(elapsedMs)} |
306 | | - </span> |
307 | | - <span |
308 | | - title={p.committed ? "已定稿待沉淀" : "识别中"} |
| 314 | + {/* 2026-08 用户需求:ASR 未沉淀行全部展示——识别中(灰斜)按句读拆多行 |
| 315 | + 全部显示;已定稿待沉淀(黑)一行;连续定稿各行并存;新句首个 |
| 316 | + partial 到达时统一沉淀入上方列表 */} |
| 317 | + {partials.map((p) => { |
| 318 | + if (p.committed) { |
| 319 | + return ( |
| 320 | + <div |
| 321 | + key={p.id} |
| 322 | + style={{ |
| 323 | + display: "flex", |
| 324 | + gap: 8, |
| 325 | + alignItems: "baseline", |
| 326 | + fontSize: 13, |
| 327 | + color: "#374151", |
| 328 | + }} |
| 329 | + > |
| 330 | + <span style={{ fontSize: 11, width: 44, flexShrink: 0, fontVariantNumeric: "tabular-nums" }}> |
| 331 | + {fmtTime(p.time)} |
| 332 | + </span> |
| 333 | + <span |
| 334 | + title="已定稿待沉淀" |
| 335 | + style={{ |
| 336 | + width: 8, |
| 337 | + height: 8, |
| 338 | + borderRadius: 4, |
| 339 | + flexShrink: 0, |
| 340 | + alignSelf: "center", |
| 341 | + background: "#9ca3af", |
| 342 | + }} |
| 343 | + /> |
| 344 | + <span>{p.text}</span> |
| 345 | + </div> |
| 346 | + ); |
| 347 | + } |
| 348 | + // 识别中:整句候选按句读切分多行灰斜体——"识别中的内容全部显示"; |
| 349 | + // 首行带时间,后续行对齐留空;残余段(无句读尾段)加 … |
| 350 | + const segs = splitBySentence(p.text); |
| 351 | + return segs.map((seg, i) => ( |
| 352 | + <div |
| 353 | + key={`${p.id}-${i}`} |
309 | 354 | style={{ |
310 | | - width: 8, |
311 | | - height: 8, |
312 | | - borderRadius: 4, |
313 | | - flexShrink: 0, |
314 | | - alignSelf: "center", |
315 | | - background: p.committed ? "#9ca3af" : "#d1d5db", |
| 355 | + display: "flex", |
| 356 | + gap: 8, |
| 357 | + alignItems: "baseline", |
| 358 | + fontSize: 13, |
| 359 | + color: "#9ca3af", |
| 360 | + fontStyle: "italic", |
316 | 361 | }} |
317 | | - /> |
318 | | - <span>{p.text}{p.committed ? "" : "…"}</span> |
319 | | - </div> |
320 | | - ))} |
| 362 | + > |
| 363 | + <span style={{ fontSize: 11, width: 44, flexShrink: 0, fontVariantNumeric: "tabular-nums" }}> |
| 364 | + {i === 0 ? fmtTime(elapsedMs) : ""} |
| 365 | + </span> |
| 366 | + <span |
| 367 | + title="识别中" |
| 368 | + style={{ |
| 369 | + width: 8, |
| 370 | + height: 8, |
| 371 | + borderRadius: 4, |
| 372 | + flexShrink: 0, |
| 373 | + alignSelf: "center", |
| 374 | + background: "#d1d5db", |
| 375 | + }} |
| 376 | + /> |
| 377 | + <span>{seg}{i === segs.length - 1 ? "…" : ""}</span> |
| 378 | + </div> |
| 379 | + )); |
| 380 | + })} |
321 | 381 | </div> |
322 | 382 | ) : ( |
323 | 383 | <div style={{ display: "flex", flexDirection: "column", gap: 3 }}> |
|
0 commit comments