Skip to content

Commit e8484cc

Browse files
ZhaoChaoqunclaude
andcommitted
fix: reset FunASR Nano LLM state after flush to prevent cross-session data leak
_accumulatedText and VAD state were not cleared between recording sessions, causing the second transcription to include text from the first session. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6d2ba65 commit e8484cc

2 files changed

Lines changed: 163 additions & 0 deletions

File tree

Sources/ASREngine.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ class FunASRNanoLLMEngine: ASREngine {
136136
}
137137

138138
let rawText = self.internalQueue.sync { self._accumulatedText }
139+
self.reset()
139140
DispatchQueue.main.async {
140141
completion(rawText)
141142
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# FunASR Nano LLM 跨录音会话数据泄漏 Bug
2+
3+
## 现象
4+
5+
使用 FunASR Nano LLM 引擎时,连续两次录音的第二次转录结果**包含了第一次的完整内容**
6+
7+
### 复现步骤
8+
9+
1. 按住 Fn 键录音第一句:"请先生成你的实现计划,如果有需要我进一步确认的地方,可以随时向我提问。"
10+
2. 松开 Fn 键,等待转录完成 → 结果正确
11+
3. 再次按住 Fn 键录音第二句:"嗯,可以帮我把本地暂未提交的代码合成比较合理的commit,然后部署到远端吗?"
12+
4. 松开 Fn 键 → **结果错误**,输出为第一句 + 第二句的拼接
13+
14+
### 实际输出
15+
16+
```
17+
请先生成你的实现计划,如果有需要我进一步确认的地方,可以随时向我提问。嗯,可以帮我把本地暂未提交的代码合成比较合理的可密的,然后部署到圆团吗?
18+
```
19+
20+
---
21+
22+
## 根因分析
23+
24+
### 核心问题
25+
26+
`FunASRNanoLLMEngine``_accumulatedText` 缓冲区和 VAD 内部状态在两次录音之间**从未被重置**`reset()` 方法存在但从未被调用。
27+
28+
### 数据流追踪
29+
30+
```
31+
第一次录音
32+
─────────────────────────────────────────────────────────────
33+
Fn 按下 → .ready → .recording
34+
35+
36+
processAudio() → vad.acceptWaveform()
37+
→ vad.popSegmentWithTime()
38+
→ recognizer.transcribe()
39+
→ _accumulatedText = "请先生成..."
40+
41+
Fn 松开 → .recording → .flushing
42+
43+
44+
flush() → vad.flush()
45+
→ 处理剩余 VAD 段
46+
→ 返回 _accumulatedText
47+
48+
49+
.flushing → .postProcessing → .ready
50+
51+
❌ 未调用 reset()
52+
❌ _accumulatedText 仍 = "请先生成..."
53+
❌ VAD 内部缓冲区未清空
54+
55+
第二次录音
56+
─────────────────────────────────────────────────────────────
57+
Fn 按下 → .ready → .recording
58+
59+
60+
processAudio() → vad.acceptWaveform()
61+
→ vad.popSegmentWithTime()
62+
→ recognizer.transcribe() → 新文本
63+
→ _accumulatedText += 新文本 ← BUG!旧数据仍在
64+
65+
66+
flush() → 返回 "第一句 + 第二句"
67+
```
68+
69+
### 涉及代码
70+
71+
#### 1. `FunASRNanoLLMEngine.flush()` — 不清理状态
72+
73+
**文件:** `Sources/ASREngine.swift:120-143`
74+
75+
```swift
76+
func flush(completion: @escaping (String) -> Void) {
77+
vad.flush()
78+
recognitionQueue.async { [weak self] in
79+
// ... 处理剩余 VAD 段 ...
80+
let rawText = self.internalQueue.sync { self._accumulatedText }
81+
DispatchQueue.main.async { completion(rawText) }
82+
// ❌ 此处缺少 reset() 调用
83+
}
84+
}
85+
```
86+
87+
`flush()` 读取并返回 `_accumulatedText`,但**不清空它**。下次录音时旧数据仍然存在。
88+
89+
#### 2. `FunASRNanoLLMEngine.reset()` — 存在但从未被调用
90+
91+
**文件:** `Sources/ASREngine.swift:145-148`
92+
93+
```swift
94+
func reset() {
95+
vad.reset()
96+
internalQueue.sync { _accumulatedText = "" }
97+
}
98+
```
99+
100+
`reset()` 实现是正确的——重置 VAD 并清空累积文本。但在整个录音生命周期中**从未被调用**
101+
102+
#### 3. `RecordingManager.handleSideEffects()` — 状态转换缺失 reset
103+
104+
**文件:** `Sources/RecordingManager.swift:132-135`
105+
106+
```swift
107+
// 后处理完成
108+
case (.postProcessing, .ready):
109+
if case .postProcessComplete(let finalText) = event {
110+
DispatchQueue.main.async { self.onFinalResult?(finalText) }
111+
}
112+
// ❌ 此处缺少 currentEngine?.reset()
113+
```
114+
115+
这是两次录音之间的唯一过渡点(`.postProcessing → .ready`),但没有调用引擎的 `reset()`
116+
117+
#### 4. `startAudioEngine()` — 也不调用 reset
118+
119+
**文件:** `Sources/RecordingManager.swift:144-176`
120+
121+
`startAudioEngine()` 只创建新的 `AVAudioEngine` 和安装 tap,不涉及 ASR 引擎的状态重置。
122+
123+
---
124+
125+
## 影响范围
126+
127+
| 引擎 | 是否受影响 | 原因 |
128+
|------|:---------:|------|
129+
| FunASR Nano LLM | **** | `_accumulatedText` 跨会话累积 |
130+
| Qwen3-ASR 流式 || `flush()` 内部调用了 `recognizer.reset()` |
131+
| Paraformer || `flush()` 内部调用了 `recognizer.reset()` |
132+
133+
对比 `QwenASREngine.flush()``ASREngine.swift:195-215`)可以看到,Qwen 引擎在 flush 末尾调用了 `self.recognizer.reset()`,所以不受此 bug 影响。
134+
135+
---
136+
137+
## 修复方案
138+
139+
`FunASRNanoLLMEngine.flush()` 的 completion 回调后调用 `self.reset()`
140+
141+
```swift
142+
func flush(completion: @escaping (String) -> Void) {
143+
vad.flush()
144+
recognitionQueue.async { [weak self] in
145+
guard let self = self else { ... }
146+
// ... 处理剩余 VAD 段 ...
147+
let rawText = self.internalQueue.sync { self._accumulatedText }
148+
self.reset() // ← 修复:flush 完成后重置状态
149+
DispatchQueue.main.async { completion(rawText) }
150+
}
151+
}
152+
```
153+
154+
这与 `QwenASREngine``ParaformerEngine` 的行为保持一致。
155+
156+
---
157+
158+
## 时间线
159+
160+
- **发现时间:** 2026-03-04 18:09
161+
- **影响版本:** FunASR Nano LLM 引擎引入以来的所有版本
162+
- **严重程度:** 高(输出错误结果,用户可感知)

0 commit comments

Comments
 (0)