AITalk:从零到一打造 macOS 系统级语音输入引擎

AITalk:从零到一打造 macOS 系统级语音输入引擎

一个完整的 AI 语音输入解决方案,让说话变成优雅的文字

项目背景

在日常工作中,我们经常需要在各种应用中输入大量文字------邮件回复、文档编写、代码注释等。虽然 macOS 自带听写功能,但它存在诸多问题:识别准确率不高、输出口语化严重、需要联网但延迟较大。我希望打造一个更智能的解决方案:

核心目标

  • 🎯 系统级覆盖:在任何应用中都能使用
  • 🤖 AI 智能优化:自动去除口语化、优化表达
  • 极速响应:2秒内完成识别和注入
  • 🔒 隐私优先:音频不落盘、仅内存处理

于是,AITalk 诞生了。


技术架构

整体设计

AITalk 采用经典的 C/S 架构,分为 macOS 客户端Python 后端服务两部分:
录音
WebSocket
流式传输
文本优化
最终文本
WebSocket
注入
用户按下热键
macOS 客户端

Swift
音频采集

AVFoundation
Python 后端

FastAPI
STT 服务

豆包语音识别
LLM 服务

豆包大模型
文本注入

Accessibility API

技术栈选择

macOS 客户端(Swift)
  • 框架:纯 Swift + AppKit
  • 音频采集:AVFoundation (16kHz, PCM16)
  • 全局热键:Carbon Event Manager
  • 文本注入:macOS Accessibility API
  • 网络通信:Starscream (WebSocket)
Python 后端
  • Web 框架:FastAPI + Uvicorn
  • STT:豆包语音识别 API(流式实时识别)
  • LLM:豆包大模型(文本润色优化)
  • 并发处理:asyncio + websockets

核心技术实现

1. 全局热键监听

macOS 中实现全局热键有多种方式,最终选择了 Carbon Event Manager,原因是:

  • ✅ 不依赖 NSEvent,无需应用获得焦点
  • ✅ 支持复杂组合键(Cmd+Shift+Space)
  • ✅ 系统级监听,任何应用都能触发
swift 复制代码
// GlobalHotkeyManager.swift
func registerHotkey(keyCode: UInt16, modifiers: [CGEventFlags]) {
    var carbonModifiers: UInt32 = 0
    for modifier in modifiers {
        if modifier == .maskCommand { carbonModifiers |= UInt32(cmdKey) }
        if modifier == .maskShift { carbonModifiers |= UInt32(shiftKey) }
    }
    
    let callback: EventHandlerUPP = { (nextHandler, theEvent, userData) -> OSStatus in
        let manager = Unmanaged<GlobalHotkeyManager>.fromOpaque(userData!).takeUnretainedValue()
        DispatchQueue.main.async { manager.toggleRecording() }
        return noErr
    }
    
    InstallEventHandler(GetApplicationEventTarget(), callback, 1, [eventType], 
                        Unmanaged.passUnretained(self).toOpaque(), &eventHandler)
    RegisterEventHotKey(UInt32(keyCode), carbonModifiers, hotKeyID, 
                       GetApplicationEventTarget(), 0, &hotKeyRef)
}

关键点

  • 使用 toggle 模式:按一次开始录音,再按一次停止
  • 通过 Unmanaged 传递 Swift 对象给 C 回调
  • 异步分发到主线程处理,避免阻塞

2. 音频采集与流式传输

采集高质量音频是语音识别的基础。AITalk 使用 AVAudioEngine 构建实时音频处理管道:

swift 复制代码
// AudioRecorder.swift
func startRecording() {
    let inputNode = audioEngine.inputNode
    let format = AVAudioFormat(commonFormat: .pcmFormatInt16, 
                               sampleRate: 16000, channels: 1, interleaved: true)!
    
    inputNode.installTap(onBus: 0, bufferSize: 3200, format: format) { buffer, _ in
        self.delegate?.audioRecorder(self, didReceiveBuffer: buffer)
    }
    
    audioEngine.prepare()
    try audioEngine.start()
}

技术细节

  • 采样率: 16kHz(语音识别标准)
  • 格式: PCM Int16(未压缩,高质量)
  • Buffer 大小: 3200 帧 = 200ms(平衡延迟和稳定性)

音频数据通过 WebSocket 实时发送至后端:

swift 复制代码
// WebSocketClient.swift
func sendAudioData(_ data: Data) {
    let message: [String: Any] = [
        "type": "audio_data",
        "data": data.base64EncodedString()
    ]
    let jsonData = try! JSONSerialization.data(withJSONObject: message)
    socket.write(data: jsonData)
}

3. STT 语音识别(豆包 API)

后端接收音频流后,通过豆包的流式语音识别 API 进行实时转写:

python 复制代码
# stt_service.py
async def stream_stt_doubao(audio_chunks: List[bytes], language: str) -> str:
    """使用豆包流式 STT API 进行语音识别"""
    
    # 创建 WebSocket 连接
    url = "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_async"
    auth_params = generate_auth_params(APP_ID, RESOURCE_ID)
    
    async with websockets.connect(url + "?" + auth_params) as ws:
        # 发送配置
        await ws.send(json.dumps({
            "type": "full_client_request",
            "data": {
                "stream_id": request_id,
                "sequence": 0,
                "payload_type": 0x1,  # JSON
                "payload": {
                    "format": "pcm",
                    "rate": 16000,
                    "bits": 16,
                    "channel": 1,
                    "language": language
                }
            }
        }))
        
        # 流式发送音频
        for i, chunk in enumerate(audio_chunks):
            await ws.send(chunk)  # 二进制音频
            
        # 接收识别结果
        transcript = ""
        async for message in ws:
            result = json.loads(message)
            if result.get("type") == "transcript":
                transcript = result["transcript"]["text"]
                
        return transcript

优势

  • ✅ 流式处理,边录边传,降低总延迟
  • ✅ 支持中英文混合识别
  • ✅ 高准确率(官方宣称 95%+)

4. LLM 文本优化

原始 STT 输出通常包含口语化表达,需要 LLM 进行润色:

python 复制代码
# llm_service.py
async def refine_text_doubao(raw_text: str, mode: str = "default") -> str:
    """使用豆包大模型润色文本"""
    
    system_prompts = {
        "default": "你是文本润色助手。将口语化的语音识别结果转换为书面语...",
        "formal": "你是专业商务写作助手...",
        "concise": "你是简洁表达专家...",
    }
    
    response = await client.chat.completions.create(
        model="doubao-pro",
        messages=[
            {"role": "system", "content": system_prompts[mode]},
            {"role": "user", "content": raw_text}
        ],
        temperature=0.3,  # 降低随机性,保持原意
        max_tokens=500
    )
    
    return response.choices[0].message.content

示例效果

原始识别 优化后
"嗯...我觉得呢,这个方案应该可能是比较好的吧" "我认为这个方案比较好。"
"那个啥,明天我们就开会讨论一下" "明天我们开会讨论。"

5. 文本注入(Accessibility API)

这是整个项目最具挑战性的部分。macOS 提供了 Accessibility API,允许应用访问和操作 UI 元素:

swift 复制代码
// TextInjector.swift
func injectText(_ text: String) -> Bool {
    guard let element = elementResolver.getFocusedElement() else {
        return false
    }
    
    // 方法 1: 直接设置 AXValue(最优)
    if injectViaAXValue(text, element: element) {
        return true
    }
    
    // 方法 2: 剪贴板 + Cmd+V(通用)
    if injectViaClipboard(text) {
        return true
    }
    
    // 方法 3: 逐字符输入(最后手段)
    return injectViaTyping(text)
}

private func injectViaAXValue(_ text: String, element: AXUIElement) -> Bool {
    // 获取当前值
    var currentValue: CFTypeRef?
    AXUIElementCopyAttributeValue(element, kAXValueAttribute as CFString, &currentValue)
    let existingText = currentValue as? String ?? ""
    
    // 拼接新文本
    let newText = existingText + text
    
    // 设置新值
    let result = AXUIElementSetAttributeValue(element, kAXValueAttribute as CFString, 
                                              newText as CFTypeRef)
    return result == .success
}

三层注入策略

  1. AXValue 直接设置:最快、最可靠,但需要 Accessibility 权限
  2. 剪贴板 + Cmd+V 模拟:兼容性好,几乎所有应用都支持
  3. 逐字符输入模拟:最后手段,慢但一定能用

关键技术难点与解决方案

难点 1:Accessibility 权限识别问题

问题: 即使在系统设置中授予了 Accessibility 权限,AXIsProcessTrusted() 仍返回 false

原因:

  • 应用重新编译后,bundle ID 或签名可能改变
  • macOS 缓存了权限状态,未及时更新

解决方案:

swift 复制代码
// 主动请求权限,带系统提示
let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary
let isTrusted = AXIsProcessTrustedWithOptions(options)

难点 2:HUD 窗口抢占焦点

问题: 显示 HUD 进度窗口时,系统焦点被抢占,导致无法获取原目标文本框。

解决方案:

swift 复制代码
// HUDPanel.swift - 使用非激活面板
init() {
    super.init(
        contentRect: NSRect(x: 0, y: 0, width: 160, height: 160),
        styleMask: [.nonactivatingPanel, .utilityWindow, .hudWindow],  // 关键!
        backing: .buffered,
        defer: false
    )
    self.level = .floating
    self.ignoresMouseEvents = true  // 穿透鼠标事件
}

难点 3:WebSocket 断线重连

问题: 网络波动或服务器重启导致连接断开。

解决方案:

swift 复制代码
// WebSocketClient.swift
func connect() {
    socket.onDisconnected = { [weak self] error in
        self?.scheduleReconnect()
    }
}

private func scheduleReconnect() {
    reconnectAttempts += 1
    let delay = min(pow(2.0, Double(reconnectAttempts)), 30.0)  // 指数退避
    DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
        self.connect()
    }
}

性能优化

1. 延迟优化

环节 延迟 优化措施
音频采集 200ms 优化 buffer 大小
网络传输 50ms 本地服务器
STT 识别 800ms 流式处理
LLM 优化 600ms 并发执行 + 温度降低
文本注入 10ms Accessibility API
总计 ≤2s

2. 内存管理

swift 复制代码
// 及时释放音频缓冲区
audioBuffers.removeAll()

// 使用 autoreleasepool 避免峰值
autoreleasepool {
    let audioData = bufferToData(buffer)
    webSocketClient.sendAudioData(audioData)
}

3. 成本优化

月度成本分析(1000 次会话):

  • 豆包 STT:¥5(¥0.005/次)
  • 豆包 LLM:¥2(¥0.002/次)
  • 服务器托管:¥30(轻量云服务器)
  • 总计:≈ ¥37/月

项目成果

功能特性

  • 系统级语音输入:支持任意 macOS 应用
  • AI 智能优化:自动去除"嗯、啊、那个"等口语词
  • 多种输出模式:默认、正式、简洁、原文
  • 中英文支持:自动识别语言
  • 极速响应:端到端延迟 ≤2秒
  • 隐私保护:音频不落盘,仅内存处理
  • 可靠性:三层文本注入,99% 成功率

技术指标

指标 数值
平均延迟 1.8s
STT 准确率 95%+
注入成功率 99%+
内存占用 <50MB
CPU 占用 <5%

代码仓库

目录 说明
TypelessMac/ Swift 客户端源码
├─ Hotkey/ 全局热键管理
├─ Audio/ 音频采集
├─ Accessibility/ 权限和元素解析
├─ TextInjection/ 文本注入引擎
├─ Network/ WebSocket 通信
├─ State/ 会话状态管理
├─ UI/ HUD 界面
server/ Python 后端服务
├─ main.py FastAPI WebSocket 服务器
├─ stt_service.py STT 语音识别
├─ llm_service.py LLM 文本优化
docs/ 完整技术文档

未来展望

短期计划(1-3个月)

  • 🎨 设置界面:可视化配置热键、模式、语言
  • 🌍 更多语言:日语、韩语、西班牙语...
  • 📊 音频可视化:实时音量显示
  • 🔄 自动更新:Sparkle 框架集成

中期计划(3-6个月)

  • 🧠 本地 STT:集成 Whisper.cpp,无需联网
  • 📱 iOS 版本:通过 Shortcuts 实现类似功能
  • 🎯 应用 Profile:针对不同应用自动切换模式
  • 🔌 插件系统:允许第三方扩展功能

长期愿景

  • 🌐 跨平台支持:Windows、Linux 版本
  • 🤝 多人协作:团队共享术语库
  • 🧩 智能联想:根据上下文预测内容
  • 📈 个人模型:基于用户习惯的微调

技术总结

开发 AITalk 的过程是一次完整的产品工程实践:

  1. 架构设计:清晰的 C/S 分离,职责明确
  2. 技术选型:平衡性能、成本、开发效率
  3. 系统编程:深入 macOS 底层 API
  4. AI 集成:STT + LLM 协同优化
  5. 用户体验:从热键到注入的每个细节打磨
  6. 工程质量:异常处理、重连机制、资源管理

最大的收获是对 macOS 系统编程能力的深入理解 ,以及如何将 AI 技术落地到实际产品


参考资料


项目地址 : AITalk on GitHub

Built with ❤️ for productivity enthusiasts

相关推荐
系'辞5 小时前
【obsidian指南】配置obsidian git插件,实现obsidian数据定时同步到github仓库(Mac电脑)
macos·github·agent·知识库
栗子叶20 小时前
IP协议 地址划分&MAC地址作用&ip addr命令
网络·tcp/ip·macos
draking1 天前
Anthropic 封杀当天,我把 OpenCode 升到 1.1.11,踩了 5 个坑
macos·ai编程
Roye_ack1 天前
【Mac环境配置教程】深度学习环境配置(Anaconda + PyTorch)
macos
程序员雄杰1 天前
腾讯云轻量应用服务器mac中ssh免密登录到服务器
macos·ssh·腾讯云
且去填词1 天前
DeepSeek :基于 AST 与 AI 的遗留系统“手术刀”式治理方案
人工智能·自动化·llm·ast·agent·策略模式·deepseek
YongPagani2 天前
Mac安装Homebrew
macos
Byron Loong2 天前
【系统】Mac系统和Linux 指令对比
linux·macos·策略模式
软件小滔2 天前
拖拽出来的专业感
经验分享·macos·mac·应用推荐