Section 16/172 menit

16. Real Use Cases

16. Real Use Cases

Use Case 1: Notes App dengan AI Search (Hybrid)

App notes dengan semantic search lokal + AI summarization opsional.

swift
// notes-ai: Architecture
@MainActor
@Observable
final class NotesVM {
    var notes: [Note] = []
    var searchResults: [NoteSearchResult] = []
    var isSearching = false

    private let store: NotesStore
    private let vectorStore: VectorStore
    private let ai: AIService

    init(store: NotesStore, vectorStore: VectorStore, ai: AIService) {
        self.store = store
        self.vectorStore = vectorStore
        self.ai = ai
    }

    func semanticSearch(_ query: String) async {
        isSearching = true
        defer { isSearching = false }
        do {
            let chunks = try await vectorStore.search(query: query, topK: 10)
            let noteIDs = Set(chunks.map(\.documentID))
            let foundNotes = try await store.notes(ids: Array(noteIDs))
            searchResults = foundNotes.map { note in
                NoteSearchResult(
                    note: note,
                    relevantSnippet: chunks.first { $0.documentID == note.id }?.content ?? ""
                )
            }
        } catch {
            searchResults = []
        }
    }

    func summarizeNote(_ note: Note) async throws -> String {
        return try await ai.summarize(note.content)
    }

    func saveNote(_ note: Note) async throws {
        try await store.save(note)
        try await vectorStore.index(documentID: note.id, content: note.content)
    }
}

struct NoteSearchResult: Identifiable {
    let id = UUID()
    let note: Note
    let relevantSnippet: String
}

Keputusan desain:

  • vectorStore untuk semantic search — selalu on-device (privat).
  • ai.summarize bisa on-device (Foundation Models) atau cloud — abstracted via protocol.
  • Index dijalankan saat save, bukan saat search (eager indexing).

Use Case 2: Receipt Scanner (Vision + LLM)

Scan struk → extract structured data → simpan ke database.

swift
// receipt-scanner: Vision OCR + Foundation Models extraction
import Vision
import FoundationModels

@Generable
struct ReceiptData: Sendable {
    let merchant: String
    let date: Date
    let totalAmount: Double
    let currency: String
    let items: [Item]

    @Generable
    struct Item: Sendable {
        let name: String
        let quantity: Int
        let price: Double
    }
}

actor ReceiptScanner {
    func scan(_ image: CGImage) async throws -> ReceiptData {
        // Step 1: OCR dengan Vision
        let request = VNRecognizeTextRequest()
        request.recognitionLevel = .accurate
        request.recognitionLanguages = ["id-ID", "en-US"]

        let handler = VNImageRequestHandler(cgImage: image)
        try handler.perform([request])

        let observations = request.results ?? []
        let text = observations.compactMap { $0.topCandidates(1).first?.string }
            .joined(separator: "\n")

        // Step 2: Extract structured data dengan Foundation Models
        let session = LanguageModelSession()
        return try await session.respond(
            to: """
            Ekstrak data dari struk berikut. Kalau tanggal/total tidak jelas, pilih yang paling mungkin.

            \(text)
            """,
            generating: ReceiptData.self
        )
    }
}

@MainActor
@Observable
final class ScanReceiptVM {
    var status: Status = .idle
    var receipt: ReceiptData?
    private let scanner: ReceiptScanner

    init(scanner: ReceiptScanner) { self.scanner = scanner }

    func scan(_ image: CGImage) async {
        status = .scanning
        do {
            let result = try await scanner.scan(image)
            self.receipt = result
            status = .done
        } catch {
            status = .error(error.localizedDescription)
        }
    }

    enum Status {
        case idle, scanning, done
        case error(String)
    }
}

Keputusan desain:

  • Vision untuk OCR (akurat untuk teks struk, gratis, offline).
  • Foundation Models untuk parsing tidak terstruktur → struktur. @Generable membuat output type-safe.
  • Semua on-device — data finansial user tidak keluar.
  • Status enum untuk UI state.

Use Case 3: Voice Assistant in-App dengan Cloud LLM

Chat suara — speech-to-text on-device, LLM cloud, TTS on-device.

swift
// voice-assistant: Pipeline lengkap
import Speech
import AVFoundation

@MainActor
@Observable
final class VoiceAssistantVM {
    var transcript: String = ""
    var aiResponse: String = ""
    var isListening = false
    var isSpeaking = false

    private let recognizer = SFSpeechRecognizer(locale: Locale(identifier: "id-ID"))!
    private let audioEngine = AVAudioEngine()
    private let synthesizer = AVSpeechSynthesizer()
    private let ai: AIService
    private var recognitionTask: SFSpeechRecognitionTask?

    init(ai: AIService) { self.ai = ai }

    func startListening() throws {
        isListening = true
        transcript = ""

        let request = SFSpeechAudioBufferRecognitionRequest()
        request.shouldReportPartialResults = true
        request.requiresOnDeviceRecognition = true  // privacy

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

        try audioEngine.start()

        recognitionTask = recognizer.recognitionTask(with: request) { [weak self] result, error in
            guard let self else { return }
            if let result {
                Task { @MainActor in
                    self.transcript = result.bestTranscription.formattedString
                    if result.isFinal {
                        await self.processQuery(self.transcript)
                    }
                }
            }
        }
    }

    func stopListening() {
        audioEngine.stop()
        audioEngine.inputNode.removeTap(onBus: 0)
        recognitionTask?.finish()
        isListening = false
    }

    private func processQuery(_ query: String) async {
        do {
            aiResponse = ""
            for try await chunk in try await ai.stream(prompt: query) {
                aiResponse += chunk
            }
            speak(aiResponse)
        } catch {
            aiResponse = "Maaf, terjadi error: \(error.localizedDescription)"
        }
    }

    private func speak(_ text: String) {
        isSpeaking = true
        let utterance = AVSpeechUtterance(string: text)
        utterance.voice = AVSpeechSynthesisVoice(language: "id-ID")
        utterance.rate = 0.5
        synthesizer.speak(utterance)
    }
}

Keputusan desain:

  • Speech-to-text requiresOnDeviceRecognition: true untuk privasi.
  • LLM cloud untuk capability tinggi (reasoning, knowledge).
  • TTS lokal lewat AVSpeechSynthesizer.
  • Streaming response — user dengar mulai jawaban sebelum semuanya selesai.

Use Case 4: Smart Compose Email dengan Privacy-Aware Fallback

Auto-suggest text completion: gunakan Foundation Models jika tersedia, fallback ke local Markov-based suggestion jika tidak.

swift
// smart-compose: Privacy-first, multi-tier
protocol TextSuggester: Sendable {
    func suggest(prefix: String, context: String) async throws -> String?
}

actor FoundationModelsSuggester: TextSuggester {
    private let session = LanguageModelSession()

    func suggest(prefix: String, context: String) async throws -> String? {
        let prompt = """
        Lanjutkan teks berikut secara natural, hanya 5-15 kata. Tanpa penjelasan.

        Konteks: \(context)
        Awal: \(prefix)
        Lanjutan:
        """
        let response = try await session.respond(to: prompt)
        return response.content
    }
}

actor LocalMarkovSuggester: TextSuggester {
    private var transitions: [String: [String]] = [:]

    func train(with corpus: [String]) {
        for text in corpus {
            let tokens = text.split(separator: " ").map(String.init)
            for i in 0..<tokens.count - 1 {
                transitions[tokens[i], default: []].append(tokens[i + 1])
            }
        }
    }

    func suggest(prefix: String, context: String) async throws -> String? {
        let tokens = prefix.split(separator: " ").map(String.init)
        guard let last = tokens.last,
              let candidates = transitions[last]?.shuffled(),
              let next = candidates.first else { return nil }
        return next
    }
}

@MainActor
@Observable
final class ComposeEmailVM {
    var draft: String = ""
    var suggestion: String?

    private let suggester: TextSuggester
    private var suggestTask: Task<Void, Never>?

    init() {
        if SystemModel.isAppleIntelligenceAvailable {
            self.suggester = FoundationModelsSuggester()
        } else {
            self.suggester = LocalMarkovSuggester()
        }
    }

    func didChangeDraft(_ newDraft: String) {
        draft = newDraft
        suggestTask?.cancel()
        suggestTask = Task { [weak self] in
            try? await Task.sleep(for: .milliseconds(400))
            guard !Task.isCancelled, let self else { return }
            do {
                let s = try await suggester.suggest(prefix: draft, context: "email")
                guard !Task.isCancelled else { return }
                self.suggestion = s
            } catch {
                self.suggestion = nil
            }
        }
    }

    func acceptSuggestion() {
        guard let suggestion else { return }
        draft += " " + suggestion
        self.suggestion = nil
    }
}

Keputusan desain:

  • Protocol TextSuggester → swappable implementasi.
  • Foundation Models untuk device support, Markov local untuk device lama → semua user dapat fitur.
  • Debounce 400ms via Task cancel pattern.
  • Tidak ada cloud — privasi total untuk email content.