You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI聊天应用语音转文字TextField残留旧转录内容问题求助

SwiftUI聊天应用语音转文字二次启动自动填充旧内容的修复方案

在SwiftUI聊天应用中实现了麦克风语音转文字功能,但存在异常:第二次长按麦克风启动新转录时,TextField会自动填充上一次的转录内容。尝试过启动新转录前清除转录内容、清空chatViewModel.currentInput等操作,问题仍未解决。

问题根源

核心问题是第一次的语音识别任务未被正确取消:即使停止了音频引擎,旧的识别任务仍在后台运行,当第二次启动转录时,旧任务的回调依然会更新currentInput,导致显示上一次的转录内容。此外,原代码未持有识别任务的引用,无法主动终止旧任务,同时启动时的状态重置不彻底。

修复方案

  • 添加SFSpeechRecognitionTask类型属性,持有当前识别任务,方便停止时主动取消
  • 启动录音前强制清空chatViewModel.currentInput和转录状态标记
  • 停止录音时不仅停止音频引擎,还要取消识别任务并重置音频会话状态

修改后的完整代码

import Speech
import SwiftUI

struct ChatView: View {
    var chatId: UUID
    @State private var isTranscriptionLongEnough = false
    private let audioEngine = AVAudioEngine()
    @EnvironmentObject var chatViewModel: ChatViewModel
    // 新增:持有当前识别任务引用,用于主动取消
    @State private var recognitionTask: SFSpeechRecognitionTask?
    
    var body: some View {
        VStack {
            ScrollViewReader { _ in
                ScrollView {
                    VStack {
                        ForEach(chatViewModel.chats.filter { $0.id == chatId }) { chat in
                            ForEach(chat.messages) { message in
                                messageView(message: message)
                            }
                        }
                    }
                    .padding()
                }
            }
            HStack {
                TextField("Enter a message...", text: $chatViewModel.currentInput)

                Button {
                    if !chatViewModel.currentInput.isEmpty {
                        chatViewModel.sendMessage(chatId: chatId)
                    }
                } label: {
                    Image(systemName: "paperplane")
                }
                Button {} label: {
                    Image(systemName: "mic.circle.fill")
                }
                .onLongPressGesture(minimumDuration: 0.1, maximumDistance: .infinity, pressing: { isPressing in
                    if isPressing {
                        startRecording()
                    } else {
                        stopRecording()
                    }
                }, perform: {})
            }
            .padding()
        }
    }

    func startRecording() {
        // 启动前清空旧内容和状态
        chatViewModel.currentInput = ""
        isTranscriptionLongEnough = false
        // 取消之前未完成的识别任务
        recognitionTask?.cancel()
        recognitionTask = nil
        
        SFSpeechRecognizer.requestAuthorization { authStatus in
            if authStatus == .authorized {
                guard let recognizer = SFSpeechRecognizer(locale: Locale.current) else { return }
                let request = SFSpeechAudioBufferRecognitionRequest()

                // 持有识别任务引用,方便后续取消
                recognitionTask = recognizer.recognitionTask(with: request) { result, error in
                    if let result = result {
                        let transcription = result.bestTranscription.formattedString
                        if !transcription.isEmpty {
                            isTranscriptionLongEnough = true
                            chatViewModel.currentInput = transcription
                        }
                    }
                    // 任务结束或出错时重置引用
                    if error != nil || result?.isFinal == true {
                        self.recognitionTask = nil
                    }
                }
                
                let audioSession = AVAudioSession.sharedInstance()
                do {
                    try audioSession.setCategory(.record, mode: .default)
                    try audioSession.setActive(true, options: .notifyOthersOnDeactivation)

                    let inputNode = audioEngine.inputNode
                    let recordingFormat = inputNode.outputFormat(forBus: 0)
                    inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { buffer, _ in
                        request.append(buffer)
                    }

                    audioEngine.prepare()
                    try audioEngine.start()
                } catch {
                    print("Error trying to transcribe audio: \(error.localizedDescription)")
                    recognitionTask = nil
                }
            }
        }
    }

    func stopRecording() {
        // 停止音频引擎并移除输入Tap
        audioEngine.stop()
        audioEngine.inputNode.removeTap(onBus: 0)
        // 取消当前识别任务
        recognitionTask?.cancel()
        recognitionTask = nil
        
        // 停用音频会话,释放资源
        let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setActive(false)
        } catch {
            print("Error deactivating audio session: \(error.localizedDescription)")
        }
        
        if isTranscriptionLongEnough {
            chatViewModel.sendMessage(chatId: chatId)
            isTranscriptionLongEnough = false
            // 发送后清空输入框
            chatViewModel.currentInput = ""
        }
    }

    func messageView(message: Message) -> some View {
        HStack {
            if message.role == .user { Spacer() }
            Text(message.content)
                .padding()
                .background(message.role == .user ? .blue : .gray.opacity(0.7))
                .foregroundColor(.white)
                .clipShape(RoundedRectangle(cornerRadius: 20))
            if message.role == .assistant || message.role == .system { Spacer() }
        }
    }
}

struct ChatView_Previews: PreviewProvider {
    static var previews: some View {
        ChatView(chatId: UUID())
            .environmentObject(ChatViewModel())
    }
}

内容的提问来源于stack exchange,提问作者Diego Perdomo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 14:10:40