Explorar el Código

Improve transcript completeness across chunk boundaries.

Use shorter overlapping chunks, preserve successful recognition locale across chunks, and remove overlap-induced duplicate prefixes to reduce dropped speech in generated transcripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
huzaifahayat12 hace 2 meses
padre
commit
3db1d72c98
Se han modificado 1 ficheros con 58 adiciones y 10 borrados
  1. 58 10
      meetings_app/Transcription/MeetingTranscriptionService.swift

+ 58 - 10
meetings_app/Transcription/MeetingTranscriptionService.swift

@@ -99,8 +99,8 @@ final class MeetingTranscriptionService {
     func transcribeMeeting(
         micURL: URL?,
         systemURL: URL?,
-        chunkSeconds: TimeInterval = 30,
-        overlapSeconds: TimeInterval = 0,
+        chunkSeconds: TimeInterval = 20,
+        overlapSeconds: TimeInterval = 1.5,
         locales: [Locale] = [],
         onProgress: (@Sendable (MeetingTranscriptionProgress) -> Void)? = nil
     ) async throws -> [TranscriptSegment] {
@@ -200,19 +200,33 @@ final class MeetingTranscriptionService {
     ) async throws -> [TranscriptSegment] {
         var segments: [TranscriptSegment] = []
         segments.reserveCapacity(chunks.count)
+        var preferredLocale: Locale?
 
         for plan in chunks {
             try Task.checkCancellation()
             let buffer = try readChunkBuffer(url: url, startFrame: plan.startFrame, frameCount: plan.frameCount)
-            let text = await transcribeBufferWithLocaleFallback(buffer: buffer, locales: locales)
+            let result = await transcribeBufferWithLocaleFallback(
+                buffer: buffer,
+                locales: locales,
+                preferredLocale: preferredLocale
+            )
             await counter.increment()
-            let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
+            preferredLocale = result.locale ?? preferredLocale
+            let trimmed = result.text.trimmingCharacters(in: .whitespacesAndNewlines)
             if trimmed.isEmpty { continue }
+            let textForSegment: String
+            if let previous = segments.last {
+                textForSegment = removeRepeatedPrefix(from: trimmed, previousText: previous.text)
+            } else {
+                textForSegment = trimmed
+            }
+            let normalized = textForSegment.trimmingCharacters(in: .whitespacesAndNewlines)
+            if normalized.isEmpty { continue }
             segments.append(TranscriptSegment(
                 speaker: speaker.rawValue,
                 startOffset: plan.startOffset,
                 endOffset: plan.endOffset,
-                text: trimmed
+                text: normalized
             ))
         }
         return segments
@@ -228,24 +242,36 @@ final class MeetingTranscriptionService {
         return buffer
     }
 
-    private func transcribeBufferWithLocaleFallback(buffer: AVAudioPCMBuffer, locales: [Locale]) async -> String {
-        for locale in locales {
+    private func transcribeBufferWithLocaleFallback(
+        buffer: AVAudioPCMBuffer,
+        locales: [Locale],
+        preferredLocale: Locale?
+    ) async -> (text: String, locale: Locale?) {
+        var orderedLocales: [Locale] = []
+        if let preferredLocale {
+            orderedLocales.append(preferredLocale)
+        }
+        for locale in locales where orderedLocales.contains(where: { $0.identifier == locale.identifier }) == false {
+            orderedLocales.append(locale)
+        }
+
+        for locale in orderedLocales {
             guard let recognizer = SFSpeechRecognizer(locale: locale), recognizer.isAvailable else { continue }
             do {
                 let text = try await transcribeBuffer(buffer: buffer, recognizer: recognizer)
                 let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
-                if trimmed.isEmpty == false { return trimmed }
+                if trimmed.isEmpty == false { return (trimmed, locale) }
             } catch {
                 // One transient retry before moving on to the next locale.
                 try? await Task.sleep(nanoseconds: 500_000_000)
                 if let text = try? await transcribeBuffer(buffer: buffer, recognizer: recognizer) {
                     let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
-                    if trimmed.isEmpty == false { return trimmed }
+                    if trimmed.isEmpty == false { return (trimmed, locale) }
                 }
                 continue
             }
         }
-        return ""
+        return ("", nil)
     }
 
     private func preferredLocalesForGlobalRecognition() -> [Locale] {
@@ -290,6 +316,28 @@ final class MeetingTranscriptionService {
         identifier.replacingOccurrences(of: "_", with: "-").lowercased()
     }
 
+    /// Removes repeated leading words from a chunk that overlap with the
+    /// trailing words of the previous chunk.
+    private func removeRepeatedPrefix(from text: String, previousText: String) -> String {
+        let separators = CharacterSet.whitespacesAndNewlines
+        let currentWords = text.components(separatedBy: separators).filter { $0.isEmpty == false }
+        let previousWords = previousText.components(separatedBy: separators).filter { $0.isEmpty == false }
+        guard currentWords.isEmpty == false, previousWords.isEmpty == false else { return text }
+
+        let maxOverlap = min(12, currentWords.count, previousWords.count)
+        guard maxOverlap > 0 else { return text }
+
+        for overlap in stride(from: maxOverlap, through: 1, by: -1) {
+            let previousSuffix = Array(previousWords.suffix(overlap)).map { $0.lowercased() }
+            let currentPrefix = Array(currentWords.prefix(overlap)).map { $0.lowercased() }
+            if previousSuffix == currentPrefix {
+                let remainder = currentWords.dropFirst(overlap)
+                return remainder.joined(separator: " ")
+            }
+        }
+        return text
+    }
+
     private func transcribeBuffer(buffer: AVAudioPCMBuffer, recognizer: SFSpeechRecognizer) async throws -> String {
         let request = SFSpeechAudioBufferRecognitionRequest()
         request.shouldReportPartialResults = false