Skip to content

Commit 3296c4d

Browse files
committed
feat(classroom): P1-4 说话人手动重识别 + P1-5 中英混说 spike
P1-4: 转写行说话人循环标注(无→A→B,飞书式重新识别),audioSegments 同步 speaker 供课后分析消费 P1-5: spike 结论落盘(本地 zipformer 双语模型混说已具备,缺口是基线验证与混说语料);设置 UI 混说说明
1 parent 6ea6f0e commit 3296c4d

7 files changed

Lines changed: 99 additions & 1 deletion

File tree

‎client/src/features/classroom/components/SessionContentView.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export function SessionContentView({ capture, onOpenNoteDialog }: SessionContent
103103
partialText={capture.partialText}
104104
isActive={capture.status === 'capturing' && (capture.mode === 'audio' || capture.mode === 'mixed')}
105105
onEditTranscript={capture.handleEditTranscript}
106+
onCycleSpeaker={capture.handleCycleSpeaker}
106107
/>
107108
</>
108109
)}

‎client/src/features/classroom/components/SettingsCollapse.tsx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ export function SettingsCollapse({ config, onChange }: SettingsCollapseProps) {
6060
</button>
6161
))}
6262
</div>
63+
{/* P1-5:混说说明(本地双语模型直接支持;云端走语言检测) */}
64+
{config.language === 'mixed' && (
65+
<p className="text-c1 text-text-tertiary mt-1">
66+
中英混说:本地双语模型直接支持;云端转写走语言检测
67+
</p>
68+
)}
6369
</div>
6470
</div>
6571
)}

‎client/src/features/classroom/components/UnifiedTimeline.tsx‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export interface TranscriptEntry {
2626
editedText?: string;
2727
/** P0-3:转写置信度(估算口径,<0.55 时 UI 弱化标记) */
2828
confidence?: number;
29+
/** P1-4:手动标注的说话人(飞书式重新识别;无/说话人A/说话人B) */
30+
speaker?: string;
2931
}
3032

3133
interface UnifiedTimelineProps {
@@ -39,6 +41,8 @@ interface UnifiedTimelineProps {
3941
isActive: boolean;
4042
/** P1-2:编辑回调——保存修正文本时调用 */
4143
onEditTranscript?: (id: string, newText: string) => void;
44+
/** P1-4:说话人循环标注回调(无 → 说话人A → 说话人B → 无) */
45+
onCycleSpeaker?: (id: string) => void;
4246
}
4347

4448
/** 事件类型 → 图标/文案/配色 */
@@ -71,7 +75,7 @@ type Row =
7175
| { kind: 'text'; ts: number; text: TranscriptEntry }
7276
| { kind: 'anchor'; ts: number; label?: string };
7377

74-
export function UnifiedTimeline({ bundle, liveTranscripts, autoAnchors = [], partialText, isActive, onEditTranscript }: UnifiedTimelineProps) {
78+
export function UnifiedTimeline({ bundle, liveTranscripts, autoAnchors = [], partialText, isActive, onEditTranscript, onCycleSpeaker }: UnifiedTimelineProps) {
7579
const scrollRef = useRef<HTMLDivElement>(null);
7680
const lastCountRef = useRef(0);
7781
const [editingId, setEditingId] = useState<string | null>(null);
@@ -193,6 +197,21 @@ export function UnifiedTimeline({ bundle, liveTranscripts, autoAnchors = [], par
193197
<div key={t.id} className="flex gap-2 p-2 rounded-kb-sm transition-colors hover:bg-bg-tertiary/30 group">
194198
<span className="text-[10px] text-text-tertiary flex-shrink-0 mt-0.5 tabular-nums">{time}</span>
195199
<Mic className="w-3.5 h-3.5 flex-shrink-0 mt-0.5 text-emerald-500/70" strokeWidth={1.5} />
200+
{/* P1-4 说话人标注(飞书式手动重新识别;课后点击循环标注) */}
201+
{!isActive && onCycleSpeaker && (
202+
<button
203+
onClick={() => onCycleSpeaker(t.id)}
204+
className={cn(
205+
'flex-shrink-0 px-1.5 py-0.5 mt-0.5 rounded-kb-sm text-[10px] font-medium border transition-all',
206+
t.speaker
207+
? 'border-cyber/40 text-cyber bg-cyber/5'
208+
: 'border-border/30 text-text-quaternary opacity-0 group-hover:opacity-100 hover:text-cyber hover:border-cyber/40',
209+
)}
210+
title={t.speaker ? `说话人: ${t.speaker}(点击切换)` : '标注说话人'}
211+
>
212+
{t.speaker ? t.speaker : '标人'}
213+
</button>
214+
)}
196215
<span className={cn('flex-1 text-[12px] leading-relaxed min-w-0', idx === rows.length - 1 ? 'text-text-primary font-medium' : 'text-text-secondary', isLowConfidence && 'opacity-60')}>
197216
{displayText}
198217
{isLowConfidence && (

‎client/src/features/classroom/hooks/useClassroomCapture.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ export function useClassroomCapture() {
351351
// 实时转录
352352
liveTranscripts: events.liveTranscripts,
353353
handleEditTranscript: events.handleEditTranscript,
354+
handleCycleSpeaker: events.handleCycleSpeaker,
354355
// 真流式进行中的 partial 文本 + 激活标志
355356
partialText: events.partialText,
356357
streamingAsrActive,

‎client/src/features/classroom/hooks/useClassroomEvents.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ export interface LiveTranscript {
5555
timestamp: number;
5656
/** P0-3:转写置信度(估算口径,<0.55 时 UI 弱化标记;缺失视为 1) */
5757
confidence?: number;
58+
/** P1-4:手动标注的说话人(飞书式重新识别) */
59+
speaker?: string;
5860
}
5961

6062
interface UseClassroomEventsOptions {
@@ -583,11 +585,33 @@ export function useClassroomEvents({
583585
}
584586
}, [liveTranscripts, courseName, toast]);
585587

588+
// P1-4:说话人循环标注(飞书式手动重新识别:无 → 说话人A → 说话人B → 无)
589+
const handleCycleSpeaker = useCallback((id: string) => {
590+
const NEXT: Array<string | undefined> = [undefined, '说话人A', '说话人B'];
591+
setLiveTranscripts((prev) =>
592+
prev.map((t) => {
593+
if (t.id !== id) return t;
594+
const cur = NEXT.indexOf(t.speaker);
595+
return { ...t, speaker: NEXT[(cur + 1) % NEXT.length] };
596+
}),
597+
);
598+
// 同步 smartBundle audioSegments(课后分析消费说话人标签)
599+
setSmartBundle((prev) => ({
600+
...prev,
601+
audioSegments: (prev.audioSegments ?? []).map((s) => {
602+
if (s.id !== id) return s;
603+
const cur = NEXT.indexOf(s.speaker);
604+
return { ...s, speaker: NEXT[(cur + 1) % NEXT.length] };
605+
}),
606+
}));
607+
}, []);
608+
586609
return {
587610
segments, setSegments, stats, setStats, extractionError, setExtractionError,
588611
smartBundle, setSmartBundle,
589612
liveTranscripts, setLiveTranscripts,
590613
handleEditTranscript,
614+
handleCycleSpeaker,
591615
partialText,
592616
vadStats, recordingStatus, setRecordingStatus,
593617
videoFilePath, setVideoFilePath,

‎client/src/lib/capture/captureTypes.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ export interface AudioSegment {
163163
audioText?: string | null;
164164
/** 原始 ASR 转写文本(热词替换前),保真溯源用 */
165165
audioTextRaw?: string | null;
166+
/** P1-4:手动标注的说话人(飞书式重新识别;未标注为 undefined) */
167+
speaker?: string;
166168
}
167169

168170
/** @ai-context 全局时间轴条目,串联关键帧和语音段供后续分析回放 */
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# 中英混说识别 Spike 结论(P1-5)
2+
3+
> 日期:2026-08-16
4+
> 目标:评估课堂「中文讲解 + 英文术语」混说场景的本地/云端识别方案
5+
> 结论来源:ASR 市场调研(`docs/Foresight/asr-market-2025-2026.md` §2)+ 仓库现有引擎代码盘点
6+
7+
---
8+
9+
## 一、现有能力盘点(代码证据)
10+
11+
| 层 | 现状 | 证据 |
12+
|---|---|---|
13+
| 本地流式引擎 | zipformer-transducer **中英双语**模型(bilingual-zh-en),天然支持混说解码;`language` 参数不参与模型选择(模型固定双语) | `client/electron/ai/local-asr/config.ts`(dirName 含 bilingual-zh-en) |
14+
| 本地重打分 | SenseVoice int8 为**多语言模型**(zh/en/ja/ko/yue),混说复核可用 | `config.ts` rescore.dirName |
15+
| 云端转写 | `language` 参数透传(zh/en/auto),Qwen3-ASR 支持 auto 语言检测;混说质量取决于 ASR 服务本身 | `server/ai-gateway/routers/transcribe.py` L34 |
16+
| 客户端语言映射 | 设置项 `mixed` → ASR 参数 `auto`(已接线) | `asrTranscriber.ts` toAsrLanguage |
17+
| 历史遗留 | 仓库曾部署过 sherpa-onnx **paraformer-bilingual-zh-en** 流式模型,后随 zipformer 切换被列入废弃清理清单(P1-1 已把 SenseVoice 移出清理清单;该双语 paraformer 目录仍在清理清单中) | `modelManager.ts` OBSOLETE_MODEL_DIRS |
18+
19+
## 二、Spike 结论
20+
21+
1. **本地混说能力已具备,无需新模型**:zipformer bilingual 的混说解码是引擎固有属性;「mixed→auto」映射对本地引擎无实际作用(模型固定双语),对云端引擎正确(auto = 语言检测)。
22+
2. **真正的缺口不是「能不能混说」,而是「混说准确率未被验证」**:评估基线(P0-1)语料规范目前未包含混说样本;无 CER 数据支撑任何优化决策。
23+
3. **市场方案对照**:FunASR `paraformer-bilingual-zh-en` 是混说专用模型(阿里云同源),sherpa-onnx 有官方转换版;若基线显示 zipformer 混说 CER 显著差于预期,可恢复该模型作为「混说专用引擎」备选(下载源与历史清理清单中目录一致,接入成本低)。
24+
4. **云端路径**:language=auto 已透传;Qwen3-ASR-Flash 的混说质量需在基线语料上实测对比(`eval.mjs --cloud` 已支持)。
25+
26+
## 三、落地动作(P1-5 实施范围)
27+
28+
- [x] 客户端语言映射已支持 mixed→auto(现状接线,无需改动)
29+
- [x] 云端 transcribe 端点 language 参数已透传(现状接线)
30+
- [ ] **基线语料补混说样本**:corpus/README 增加混说样本要求(≥2 节含英文术语课程),CER 对比 zipformer vs 云端 auto
31+
- [ ] **设置 UI 提示**:识别语言「中英混说」选项补充说明(本地双语模型直接支持;云端走语言检测)
32+
- [ ] 若基线混说 CER > 阈值(相对纯中文 +50%):恢复 paraformer-bilingual 引擎为可选下载项(config.ts 增加第三个模型定义 + 引擎路由)
33+
34+
## 四、验收口径
35+
36+
- 混说样本英文术语不被音译错字(如「python」不被转成「派森」)——热词命中率口径(P0-1)覆盖
37+
- 混说 CER 与纯中文 CER 差距 ≤50%(基线对比)
38+
39+
---
40+
41+
## 附:相关文件
42+
43+
- 市场调研:`docs/Foresight/asr-market-2025-2026.md`(§2 中英混说)
44+
- 评估基线:`client/scripts/asr-eval/`、`docs/asr-eval-baseline/BASELINE-2026-08-16.md`
45+
- 引擎配置:`client/electron/ai/local-asr/config.ts`、`modelManager.ts`

0 commit comments

Comments
 (0)