A Swift Package for an on-device Korean Socratic dialogue engine — Gemma 4 E4B (4-bit MLX), 4-function dispatch, 16 visemes, zero network egress. The bust UI is one consumer of this package. Drop it into your own app in three lines.
Add the dependency, then the product. SocraticEngine vends a single library product and pulls MLX, swift-huggingface, and swift-transformers transitively.
// 1. add the package .package(url: "https://github.com/Two-Weeks-Team/he-was-socrates", from: "1.0.0"), // 2. depend on the product from your target .product(name: "SocraticEngine", package: "he-was-socrates"), // 3. that's it. no network entitlement required by the package itself.
Instantiate EngineCoordinator as @MainActor,
observe Phase, push audio in, get TurnOutput out.
No UI required — this snippet is enough to run a full turn.
import SocraticEngine @MainActor func runOneTurn() async throws { let engine = EngineCoordinator(mode: .real) // or .stub for tests try await engine.bootstrap() // loads Gemma 4 E4B 4-bit weights for await phase in engine.phaseStream { // bootstrapping → idle → … → speaking print("phase:", phase) } try await engine.beginListening() // push-to-talk start let turn: TurnOutput = try await engine.endListening() print(turn.koreanResponse, turn.mode) // 평어체 text + Mode enum }
Stable surface per runs/2026-05-05-spec/spec/SPEC.md.
Shape changes only via delta documents at the same path.
| Symbol | Kind | Description |
|---|---|---|
| EngineCoordinator | @MainActor class | The only type the host app instantiates. Composes the six subsystems and exposes Phase + TurnOutput. |
| EngineCoordinator.Phase | enum | bootstrapping → idle → listening → thinking → surfacing → speaking → failed. Bind your UI to this stream. |
| FunctionCallOrchestrator | actor | 4-function dispatch: mode_classify, surface_past_wonder, ask_back, defer_to_human. Abstention is a first-class branch, not a fallback. |
| GemmaService | protocol | .real wires LLMRegistry.gemma4_e4b_it_4bit; .stub returns canned Korean Socratic JSON for tests. |
| AudioInputManager | @MainActor class | SFSpeechRecognizer with requiresOnDeviceRecognition = true + AVAudioEngine push-to-talk. macOS 26 SpeechAnalyzer path available. |
| TTSManager | @MainActor class | AVSpeechSynthesizer; Yuna (ko) / Samantha (en); premium → enhanced → default fallback. Emits phoneme markers when available. |
| VisemeDriver | @MainActor class | 30 fps tick, ≥2-frame hold, audio-clock synced. Apple phoneme markers primary; JamoTimeline Korean fallback. 12 fps under Reduce Motion. |
| VisemeID | enum (16 cases) | 16 mouth-shape identifiers. Maps 1:1 to the 16 halftone PNGs in the asset pipeline. PhonemeMap.default bridges from IPA + jamo. |
| Mode | enum | Result of mode_classify. Selects which of the four functions the orchestrator routes to next. |
| TurnOutput | struct | One turn's result: Korean response text (단정한 평어체), selected Mode, phoneme/jamo timeline, optional surfaced past wonder. |
| WonderingLog | actor | Core Data persistence; SHA-256 fingerprint dedup; deterministic JSON export. Phase 4 multi-year recall is wiring-stage; surfacing logic ships now. |
The same Swift Package powers all three. The only thing that changes across consumers is presentation; the turn loop, abstention rules, and Korean tone travel with the package.
import SocraticEngine import SwiftUI @main struct App: App { @State var e = EngineCoordinator(.real) var body: some Scene { WindowGroup { ContentView(engine: e) } } }
Fullscreen halftone bust + 16 viseme swap. The reference UI.
import SocraticEngine @main struct Repl { static func main() async throws { let e = EngineCoordinator(.real) try await e.bootstrap() while let line = readLine() { let t = try await e.runText(line) print(t.koreanResponse) } } }
Headless terminal repl. Same engine, no AppKit.
// Planned: same EngineCoordinator, // RealityKit halftone material instead // of NSImageView swap. import SocraticEngine import RealityKit // platform floor blocks shipping today; // API surface is already compatible.
Marked future. Listed to show the SDK is portable across hosts, not bound to AppKit.
These are properties of the package, not of the example app. Any
host that depends on SocraticEngine gets them for free.
SystemPrompt.swift is embedded at compile time. Embedders
inherit the tone whether they want it or not — this is the contract,
not a default.