TranscripteurSpeech.swift
99 lignes · 3329 octets
import AVFoundation import Speech /// Transcription locale via SFSpeechRecognizer (on-device, fr_FR). /// /// `requiresOnDeviceRecognition = true` garantit que les données audio /// ne quittent pas l'appareil. /// /// Gère gracieusement le refus de permission et l'indisponibilité du moteur : /// `etat` vaut `.indisponible` dans ces cas, et `accepterEchantillons` retourne `nil`. final class TranscripteurSpeech: TranscripteurLocal { private let reconnaissance: SFSpeechRecognizer? private var requete: SFSpeechAudioBufferRecognitionRequest? private var tache: SFSpeechRecognitionTask? private var dernierPartiel: String = "" private var texteFinal: String = "" // Float32 non-entrelacé : format le plus compatible avec SFSpeechRecognizer. private let formatFloat: AVAudioFormat = AVAudioFormat( commonFormat: .pcmFormatFloat32, sampleRate: 16_000, channels: 1, interleaved: false )! init() { reconnaissance = SFSpeechRecognizer(locale: Locale(identifier: "fr_FR")) reconnaissance?.defaultTaskHint = .dictation } // MARK: - TranscripteurLocal var etat: EtatTranscripteur { guard let r = reconnaissance, r.isAvailable else { return .indisponible } guard SFSpeechRecognizer.authorizationStatus() == .authorized else { return .indisponible } return .disponible } func accepterEchantillons(_ data: [Int16]) -> String? { guard etat == .disponible else { return nil } // Démarre la tâche de reconnaissance au premier appel de la session. if requete == nil { demarrerTache() } if let buffer = creerBuffer(depuis: data) { requete?.append(buffer) } return dernierPartiel.isEmpty ? nil : dernierPartiel } func finaliser() -> String { requete?.endAudio() return texteFinal.isEmpty ? dernierPartiel : texteFinal } func reinitialiser() { tache?.cancel() tache = nil requete = nil dernierPartiel = "" texteFinal = "" } // MARK: - Privé private func demarrerTache() { let req = SFSpeechAudioBufferRecognitionRequest() req.requiresOnDeviceRecognition = true req.shouldReportPartialResults = true requete = req tache = reconnaissance?.recognitionTask(with: req) { [weak self] result, _ in guard let self, let result else { return } let texte = result.bestTranscription.formattedString if result.isFinal { self.texteFinal = texte self.dernierPartiel = "" } else { self.dernierPartiel = texte } } } /// Convertit un tableau Int16 en AVAudioPCMBuffer Float32 pour SFSpeechRecognizer. private func creerBuffer(depuis data: [Int16]) -> AVAudioPCMBuffer? { let frameCount = AVAudioFrameCount(data.count) guard let buffer = AVAudioPCMBuffer(pcmFormat: formatFloat, frameCapacity: frameCount), let channelData = buffer.floatChannelData else { return nil } buffer.frameLength = frameCount for (i, sample) in data.enumerated() { channelData[0][i] = Float(sample) / 32_768.0 } return buffer } }
GitRust