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)
}
}