Skip to content

Commit f0dd267

Browse files
ZhaoChaoqunclaude
andcommitted
Add intelligent punctuation based on VAD segmentation
- Add SpeechSegment struct with timing info (startTime/endTime) - Detect pause duration between speech segments - Insert comma for short pauses (<1s), period for long pauses (>=1s) - Auto-detect question words (吗呢吧么嘛) and add question mark - Auto-detect exclamation words (哇耶啦) and add exclamation mark - Add final punctuation when recording stops Co-Authored-By: Claude <noreply@anthropic.com>
1 parent c773039 commit f0dd267

3 files changed

Lines changed: 94 additions & 19 deletions

File tree

Sources/RecordingManager.swift

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ class RecordingManager {
1515
var onPartialResult: ((String) -> Void)?
1616
/// 累积的识别文字
1717
private var accumulatedText: String = ""
18+
/// 上一个语音段的结束时间(用于计算停顿时长)
19+
private var lastSegmentEndTime: Float = 0
1820
/// 用于识别的队列
1921
private let recognitionQueue = DispatchQueue(label: "com.typeless.recognition", qos: .userInitiated)
2022

@@ -78,6 +80,7 @@ class RecordingManager {
7880

7981
// 重置状态
8082
accumulatedText = ""
83+
lastSegmentEndTime = 0
8184
vad?.reset()
8285

8386
// 创建音频引擎
@@ -145,25 +148,30 @@ class RecordingManager {
145148

146149
// 检查是否有完整的语音段
147150
while vad.hasSegment() {
148-
if let segment = vad.popSegment() {
151+
if let segment = vad.popSegmentWithTime() {
149152
recognitionQueue.async { [weak self] in
150153
self?.transcribeSegment(segment)
151154
}
152155
}
153156
}
154157
}
155158

156-
private func transcribeSegment(_ samples: [Float]) {
159+
private func transcribeSegment(_ segment: SpeechSegment) {
157160
guard let recognizer = recognizer else { return }
158161

159-
if let text = recognizer.transcribe(samples: samples) {
162+
if let text = recognizer.transcribe(samples: segment.samples) {
160163
DispatchQueue.main.async { [weak self] in
161164
guard let self = self else { return }
162165

163-
// 智能拼接文字(去除重叠)
164-
self.accumulatedText = self.mergeTexts(self.accumulatedText, text)
166+
// 计算与上一段的停顿时长
167+
let pauseDuration = self.lastSegmentEndTime > 0 ? segment.startTime - self.lastSegmentEndTime : 0
168+
self.lastSegmentEndTime = segment.endTime
169+
170+
// 智能拼接文字(带标点)
171+
self.accumulatedText = self.mergeTexts(self.accumulatedText, text, pauseDuration: pauseDuration)
165172

166173
print(">>> 分段识别结果: \(text)")
174+
print(">>> 停顿时长: \(pauseDuration)")
167175
print(">>> 累积文字: \(self.accumulatedText)")
168176

169177
// 通知 UI 更新
@@ -172,9 +180,53 @@ class RecordingManager {
172180
}
173181
}
174182

175-
/// 拼接文字
176-
private func mergeTexts(_ existing: String, _ new: String) -> String {
177-
return existing + new
183+
/// 根据文本内容和停顿时长决定标点
184+
private func determinePunctuation(text: String, pauseDuration: Float) -> String {
185+
let trimmed = text.trimmingCharacters(in: .whitespaces)
186+
guard let lastChar = trimmed.last else { return "" }
187+
188+
// 疑问词检测
189+
let questionWords: Set<Character> = ["", "", "", "", ""]
190+
if questionWords.contains(lastChar) {
191+
return ""
192+
}
193+
194+
// 感叹词检测
195+
let exclamationWords: Set<Character> = ["", "", ""]
196+
if exclamationWords.contains(lastChar) {
197+
return ""
198+
}
199+
200+
// 根据停顿时长决定
201+
return pauseDuration >= 1.0 ? "" : ""
202+
}
203+
204+
/// 拼接文字(带智能标点)
205+
private func mergeTexts(_ existing: String, _ new: String, pauseDuration: Float) -> String {
206+
if existing.isEmpty {
207+
return new
208+
}
209+
let punctuation = determinePunctuation(text: existing, pauseDuration: pauseDuration)
210+
return existing + punctuation + new
211+
}
212+
213+
/// 在文本末尾添加最终标点
214+
private func addFinalPunctuation(_ text: String) -> String {
215+
let punctuationSet: Set<Character> = ["", "", "", "", "", ""]
216+
if let last = text.last, punctuationSet.contains(last) {
217+
return text
218+
}
219+
220+
// 检查是否应该用问号
221+
let trimmed = text.trimmingCharacters(in: .whitespaces)
222+
if let lastChar = trimmed.last {
223+
let questionWords: Set<Character> = ["", "", "", "", ""]
224+
if questionWords.contains(lastChar) {
225+
return text + ""
226+
}
227+
}
228+
229+
return text + ""
178230
}
179231

180232
func stopRecording(completion: @escaping (String?) -> Void) {
@@ -201,18 +253,24 @@ class RecordingManager {
201253
}
202254

203255
while self.vad?.hasSegment() == true {
204-
if let segment = self.vad?.popSegment() {
205-
if let text = self.recognizer?.transcribe(samples: segment) {
256+
if let segment = self.vad?.popSegmentWithTime() {
257+
if let text = self.recognizer?.transcribe(samples: segment.samples) {
206258
DispatchQueue.main.sync {
207-
// 智能拼接文字(去除重叠)
208-
self.accumulatedText = self.mergeTexts(self.accumulatedText, text)
259+
// 计算停顿时长
260+
let pauseDuration = self.lastSegmentEndTime > 0 ? segment.startTime - self.lastSegmentEndTime : 0
261+
self.lastSegmentEndTime = segment.endTime
262+
// 智能拼接文字(带标点)
263+
self.accumulatedText = self.mergeTexts(self.accumulatedText, text, pauseDuration: pauseDuration)
209264
}
210265
}
211266
}
212267
}
213268

214-
// 返回最终结果
215-
let finalText = self.accumulatedText.isEmpty ? nil : self.accumulatedText
269+
// 返回最终结果(添加末尾标点)
270+
var finalText: String? = nil
271+
if !self.accumulatedText.isEmpty {
272+
finalText = self.addFinalPunctuation(self.accumulatedText)
273+
}
216274
DispatchQueue.main.async {
217275
print(">>> 最终识别结果: \(finalText ?? "(无)")")
218276
completion(finalText)

Sources/SherpaOnnxVAD.swift

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import Foundation
22

3+
/// 语音段数据结构
4+
struct SpeechSegment {
5+
let samples: [Float]
6+
let startTime: Float // 开始时间(秒)
7+
let endTime: Float // 结束时间(秒)
8+
}
9+
310
/// Sherpa-ONNX VAD(语音活动检测)
411
class SherpaOnnxVAD {
512
private var vad: OpaquePointer?
@@ -68,8 +75,8 @@ class SherpaOnnxVAD {
6875
return SherpaOnnxVoiceActivityDetectorDetected(vad) == 1
6976
}
7077

71-
/// 获取并移除第一个语音段
72-
func popSegment() -> [Float]? {
78+
/// 获取并移除第一个语音段(包含时间信息)
79+
func popSegmentWithTime() -> SpeechSegment? {
7380
guard let vad = vad else { return nil }
7481
guard hasSegment() else { return nil }
7582

@@ -93,7 +100,17 @@ class SherpaOnnxVAD {
93100
samples[i] = samplesPtr[i]
94101
}
95102

96-
return samples
103+
// 计算时间戳(采样率 16000Hz)
104+
let startSample = Int(segment.pointee.start)
105+
let startTime = Float(startSample) / Float(sampleRate)
106+
let endTime = Float(startSample + count) / Float(sampleRate)
107+
108+
return SpeechSegment(samples: samples, startTime: startTime, endTime: endTime)
109+
}
110+
111+
/// 获取并移除第一个语音段(仅返回样本,向后兼容)
112+
func popSegment() -> [Float]? {
113+
return popSegmentWithTime()?.samples
97114
}
98115

99116
/// 刷新缓冲区,处理剩余数据

Typeless.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@
357357
"$(inherited)",
358358
"$(SRCROOT)/Frameworks/sherpa-onnx/lib",
359359
);
360-
MARKETING_VERSION = 1.2.5;
360+
MARKETING_VERSION = 1.2.6;
361361
PRODUCT_BUNDLE_IDENTIFIER = com.typeless.app;
362362
PRODUCT_NAME = "Nano Typeless";
363363
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -393,7 +393,7 @@
393393
"$(inherited)",
394394
"$(SRCROOT)/Frameworks/sherpa-onnx/lib",
395395
);
396-
MARKETING_VERSION = 1.2.5;
396+
MARKETING_VERSION = 1.2.6;
397397
PRODUCT_BUNDLE_IDENTIFIER = com.typeless.app;
398398
PRODUCT_NAME = "Nano Typeless";
399399
SWIFT_EMIT_LOC_STRINGS = YES;

0 commit comments

Comments
 (0)