Skip to content

Commit 6b62142

Browse files
ZhaoChaoqunclaude
andcommitted
Add VAD-based real-time speech recognition feedback
- Add SherpaOnnxVAD for voice activity detection with Silero model - Implement streaming audio capture using AVAudioEngine - Show real-time recognition results in overlay HUD while speaking - Add intelligent text merging to handle segment overlaps - Add VAD model auto-download support - Bump version to 1.2.4 Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 12d2578 commit 6b62142

7 files changed

Lines changed: 458 additions & 67 deletions

File tree

Sources/OverlayWindow.swift

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class OverlayWindowController {
1717

1818
// 创建一个无边框的悬浮窗口
1919
window = NSWindow(
20-
contentRect: NSRect(x: 0, y: 0, width: 200, height: 36),
20+
contentRect: NSRect(x: 0, y: 0, width: 320, height: 80),
2121
styleMask: [.borderless],
2222
backing: .buffered,
2323
defer: false
@@ -54,6 +54,7 @@ class OverlayWindowController {
5454
func show() {
5555
print(">>> OverlayWindow show() called")
5656
viewModel.state = .recording
57+
viewModel.reset()
5758
viewModel.startAnimation()
5859
positionWindow()
5960
window?.orderFront(nil)
@@ -66,6 +67,14 @@ class OverlayWindowController {
6667
positionWindow()
6768
}
6869

70+
func updateRecognizedText(_ text: String) {
71+
viewModel.recognizedText = text
72+
// 窗口内容变化时重新定位
73+
DispatchQueue.main.async { [weak self] in
74+
self?.positionWindow()
75+
}
76+
}
77+
6978
func hide() {
7079
print(">>> OverlayWindow hide() called")
7180
viewModel.stopAnimation()
@@ -82,6 +91,7 @@ class OverlayViewModel: ObservableObject {
8291

8392
@Published var state: State = .recording
8493
@Published var animationPhase: CGFloat = 0
94+
@Published var recognizedText: String = ""
8595

8696
private var animationTimer: Timer?
8797

@@ -96,47 +106,66 @@ class OverlayViewModel: ObservableObject {
96106
animationTimer?.invalidate()
97107
animationTimer = nil
98108
}
109+
110+
func reset() {
111+
recognizedText = ""
112+
}
99113
}
100114

101115
/// 悬浮窗口视图 - 类似截图中的样式
102116
struct OverlayView: View {
103117
@ObservedObject var viewModel: OverlayViewModel
104118

105119
var body: some View {
106-
HStack(spacing: 12) {
107-
// 左侧图标区域 - 固定宽度
108-
Group {
109-
if viewModel.state == .recording {
110-
// 录音动画 - 竖纹声波
111-
HStack(spacing: 3) {
112-
ForEach(0..<7, id: \.self) { index in
113-
RoundedRectangle(cornerRadius: 1.5)
114-
.fill(Color.white.opacity(0.9))
115-
.frame(width: 3, height: waveHeight(for: index))
120+
VStack(spacing: 8) {
121+
// 主状态指示器
122+
HStack(spacing: 12) {
123+
// 左侧图标区域 - 固定宽度
124+
Group {
125+
if viewModel.state == .recording {
126+
// 录音动画 - 竖纹声波
127+
HStack(spacing: 3) {
128+
ForEach(0..<7, id: \.self) { index in
129+
RoundedRectangle(cornerRadius: 1.5)
130+
.fill(Color.white.opacity(0.9))
131+
.frame(width: 3, height: waveHeight(for: index))
132+
}
116133
}
134+
} else {
135+
// 处理中 - 显示旋转指示器
136+
ProgressView()
137+
.progressViewStyle(CircularProgressViewStyle(tint: .white))
138+
.scaleEffect(0.7)
117139
}
118-
} else {
119-
// 处理中 - 显示旋转指示器
120-
ProgressView()
121-
.progressViewStyle(CircularProgressViewStyle(tint: .white))
122-
.scaleEffect(0.7)
123140
}
141+
.frame(width: 30, height: 18)
142+
143+
// 右侧文字区域 - 固定宽度确保一致
144+
Text(viewModel.state == .recording ? "正在聆听..." : "识别中...")
145+
.font(.system(size: 13, weight: .medium))
146+
.foregroundColor(.white)
147+
.frame(width: 75, alignment: .leading)
124148
}
125-
.frame(width: 30, height: 18)
126149

127-
// 右侧文字区域 - 固定宽度确保一致
128-
Text(viewModel.state == .recording ? "正在聆听..." : "识别中...")
129-
.font(.system(size: 13, weight: .medium))
130-
.foregroundColor(.white)
131-
.frame(width: 75, alignment: .leading)
150+
// 识别结果显示(当有文字时)
151+
if !viewModel.recognizedText.isEmpty {
152+
Text(viewModel.recognizedText)
153+
.font(.system(size: 13))
154+
.foregroundColor(.white.opacity(0.9))
155+
.lineLimit(2)
156+
.truncationMode(.head)
157+
.frame(maxWidth: 280)
158+
.multilineTextAlignment(.center)
159+
}
132160
}
133161
.padding(.horizontal, 16)
134-
.padding(.vertical, 8)
162+
.padding(.vertical, 10)
135163
.background(
136164
Capsule()
137165
.fill(Color.black.opacity(0.75))
138166
)
139167
.animation(.easeInOut(duration: 0.1), value: viewModel.animationPhase)
168+
.animation(.easeInOut(duration: 0.2), value: viewModel.recognizedText)
140169
}
141170

142171
private func waveHeight(for index: Int) -> CGFloat {

0 commit comments

Comments
 (0)