Section 9/151 menit

9. Menggabungkan Multiple AI Frameworks

9. Menggabungkan Multiple AI Frameworks

Pipeline: Gambar → OCR → NER → Insights

swift
import Vision
import NaturalLanguage

// Pipeline: foto kartu nama → ekstrak teks → ekstrak entities

@MainActor
class BusinessCardProcessor {

    struct BusinessCardInfo {
        var names: [String] = []
        var organizations: [String] = []
        var phoneNumbers: [String] = []
        var emails: [String] = []
        var rawText: String = ""
    }

    func processBusinessCard(_ image: UIImage) async throws -> BusinessCardInfo {
        // Step 1: OCR via Vision
        guard let cgImage = image.cgImage else {
            throw ProcessingError.invalidImage
        }

        let textBlocks = try await performOCR(on: cgImage)
        let fullText = textBlocks.joined(separator: "\n")

        // Step 2: NER via NaturalLanguage
        var info = BusinessCardInfo(rawText: fullText)

        let tagger = NLTagger(tagSchemes: [.nameType])
        tagger.string = fullText

        tagger.enumerateTags(
            in: fullText.startIndex..<fullText.endIndex,
            unit: .word,
            scheme: .nameType,
            options: [.joinNames]
        ) { tag, range in
            let entity = String(fullText[range])
            switch tag {
            case .personalName: info.names.append(entity)
            case .organizationName: info.organizations.append(entity)
            default: break
            }
            return true
        }

        // Step 3: Regex untuk phone/email (Vision OCR tidak selalu perfect)
        info.phoneNumbers = extractPhoneNumbers(from: fullText)
        info.emails = extractEmails(from: fullText)

        return info
    }

    private func performOCR(on cgImage: CGImage) async throws -> [String] {
        try await withCheckedThrowingContinuation { continuation in
            let request = VNRecognizeTextRequest { request, error in
                if let error { continuation.resume(throwing: error); return }
                let texts = request.results?
                    .compactMap { $0.topCandidates(1).first?.string } ?? []
                continuation.resume(returning: texts)
            }
            request.recognitionLevel = .accurate
            request.usesLanguageCorrection = true

            try? VNImageRequestHandler(cgImage: cgImage).perform([request])
        }
    }

    private func extractPhoneNumbers(from text: String) -> [String] {
        let pattern = #"(\+62|0)[\s-]?(\d{3,4})[\s-]?(\d{3,4})[\s-]?(\d{3,5})"#
        let regex = try? NSRegularExpression(pattern: pattern)
        let range = NSRange(text.startIndex..., in: text)
        return regex?.matches(in: text, range: range)
            .compactMap { Range($0.range, in: text).map { String(text[$0]) } } ?? []
    }

    private func extractEmails(from text: String) -> [String] {
        let pattern = #"[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}"#
        let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive)
        let range = NSRange(text.startIndex..., in: text)
        return regex?.matches(in: text, range: range)
            .compactMap { Range($0.range, in: text).map { String(text[$0]) } } ?? []
    }
}

Pipeline: Audio → Transkripsi → Sentiment → Summary

swift
// Pipeline: rekaman meeting → transkripsi → analisis sentiment per segmen

import Speech
import NaturalLanguage

class MeetingAnalyzer {

    struct MeetingInsight {
        let transcript: String
        let segments: [Segment]
        let overallSentiment: Double
        let keyTopics: [String]

        struct Segment {
            let text: String
            let sentiment: Double
            let timeRange: Range<TimeInterval>
        }
    }

    func analyzeMeeting(audioURL: URL) async throws -> MeetingInsight {
        // Step 1: Speech-to-text
        let transcript = try await transcribeAudio(at: audioURL)

        // Step 2: Tokenize ke segmen (kalimat)
        let tagger = NLTagger(tagSchemes: [.sentimentScore, .lexicalClass])
        tagger.string = transcript

        var segments: [MeetingInsight.Segment] = []
        var sentenceIndex = 0

        tagger.enumerateTags(
            in: transcript.startIndex..<transcript.endIndex,
            unit: .sentence,
            scheme: .sentimentScore
        ) { tag, range in
            let sentence = String(transcript[range])
            let score = Double(tag?.rawValue ?? "0") ?? 0.0
            let approximateTime = Double(sentenceIndex) * 3.0  // ~3 detik per kalimat

            segments.append(MeetingInsight.Segment(
                text: sentence,
                sentiment: score,
                timeRange: approximateTime..<(approximateTime + 3.0)
            ))
            sentenceIndex += 1
            return true
        }

        // Step 3: Key topics via noun extraction
        let keyTopics = extractKeyNouns(from: transcript)

        let overallSentiment = segments.isEmpty ? 0 : segments.map(\.sentiment).reduce(0, +) / Double(segments.count)

        return MeetingInsight(
            transcript: transcript,
            segments: segments,
            overallSentiment: overallSentiment,
            keyTopics: keyTopics
        )
    }

    private func transcribeAudio(at url: URL) async throws -> String {
        let recognizer = SFSpeechRecognizer(locale: Locale(identifier: "id-ID"))!
        let request = SFSpeechURLRecognitionRequest(url: url)
        request.shouldReportPartialResults = false
        request.requiresOnDeviceRecognition = true

        return try await withCheckedThrowingContinuation { continuation in
            recognizer.recognitionTask(with: request) { result, error in
                if let error { continuation.resume(throwing: error); return }
                if result?.isFinal == true {
                    continuation.resume(returning: result?.bestTranscription.formattedString ?? "")
                }
            }
        }
    }

    private func extractKeyNouns(from text: String) -> [String] {
        var nounCount: [String: Int] = [:]
        let tagger = NLTagger(tagSchemes: [.lexicalClass])
        tagger.string = text

        tagger.enumerateTags(
            in: text.startIndex..<text.endIndex,
            unit: .word,
            scheme: .lexicalClass,
            options: [.omitPunctuation, .omitWhitespace]
        ) { tag, range in
            if tag == .noun {
                let word = String(text[range]).lowercased()
                if word.count > 3 {  // filter kata pendek
                    nounCount[word, default: 0] += 1
                }
            }
            return true
        }

        return nounCount
            .sorted { $0.value > $1.value }
            .prefix(10)
            .map(\.key)
    }
}