// Copyright 2026 Everpaper. Licensed under the Apache License, Version 2.0 (see ../../../LICENSE). // // eg2-cli — command-line front end for the EmbeddingGemma2CoreAI library. // // eg2-cli embed [--compute ane|gpu|cpu] [--kind document|search_query|...] [--dim 768] "text" ... // eg2-cli embed-image [--compute ...] [--image-compute ane|gpu|cpu] [--exif] [--dim 768] image ... // (one JSON line per image) // eg2-cli embed-audio [--compute ...] [--audio-compute ane|gpu|cpu] [--long truncate|chunk_mean] [--dim 768] clip ... // (one JSON line per clip) // eg2-cli smoke [ ...] [--compute ...] [--image-compute ...] [--audio-compute ...] [--cleanup-temp] // (text: tests/smoke-reference.json; images: tests/image-smoke-reference.json; audio: tests/audio-smoke-reference.json) // eg2-cli tokcheck // eg2-cli pixcheck (decode + resize vs the Python reference, no model needed) // eg2-cli audiocheck (decode + resample + log-mel vs the Python reference, no model needed) // eg2-cli dump [--exif] (writes .decoded.rgb / .resized.rgb / .json) // eg2-cli dump-audio (writes .wave16k.f32 / .features.f32 / .json) // eg2-cli latency [--compute ...] [--image-compute ...] [--audio-compute ...] [--trials 50] [--text-only] // eg2-cli prewarm [--compute ...] [--image-compute ...] [--audio-compute ...] [--text-only] // eg2-cli cleanup-temp (removes this process's MPSGraph temp folders; mostly useful after the others) // // `embed` and `tokcheck` load the text asset only; `embed-image` also loads the vision asset and `embed-audio` the // audio asset; `smoke` loads what its reference files need; `latency` and `prewarm` load every asset the bundle has // unless --text-only. Every command accepts --cleanup-temp to delete the MPSGraph temp folders this process created, // after the model is released (see EmbeddingGemma2.removeOwnMPSGraphTempFiles). `--compute` places the text asset // (and the image and audio encoders unless `--image-compute` / `--audio-compute` are given). import CryptoKit import EmbeddingGemma2CoreAI import Foundation var args = Array(CommandLine.arguments.dropFirst()) @MainActor func usage() -> Never { print(""" usage: eg2-cli ... [--compute ane|gpu|cpu] [--image-compute ane|gpu|cpu] [--audio-compute ane|gpu|cpu] [--cleanup-temp] """) exit(2) } @MainActor func takeOption(_ name: String) -> String? { guard let i = args.firstIndex(of: name), i + 1 < args.count else { return nil } let v = args[i + 1] args.removeSubrange(i...(i + 1)) return v } @MainActor func takeFlag(_ name: String) -> Bool { guard let i = args.firstIndex(of: name) else { return false } args.remove(at: i) return true } func fmt(_ x: Double, _ digits: Int = 2) -> String { String(format: "%.\(digits)f", x) } func sha256(_ bytes: [UInt8]) -> String { SHA256.hash(data: Data(bytes)).map { String(format: "%02x", $0) }.joined() } func cpuSeconds() -> Double { var u = rusage() getrusage(RUSAGE_SELF, &u) return Double(u.ru_utime.tv_sec) + Double(u.ru_utime.tv_usec) / 1e6 + Double(u.ru_stime.tv_sec) + Double(u.ru_stime.tv_usec) / 1e6 } /// Peak physical footprint (MB) and the Neural Engine footprint (MB) of this process (proc_pid_rusage, RUSAGE_INFO_V6). func footprints() -> (physMB: Double, neuralMB: Double) { var r = rusage_info_v6() let kr = withUnsafeMutablePointer(to: &r) { p -> Int32 in p.withMemoryRebound(to: rusage_info_t?.self, capacity: 1) { proc_pid_rusage(getpid(), RUSAGE_INFO_V6, $0) } } guard kr == 0 else { return (-1, -1) } return (Double(r.ri_lifetime_max_phys_footprint) / 1048576, Double(r.ri_neural_footprint) / 1048576) } func percentile(_ xs: [Double], _ p: Double) -> Double { let s = xs.sorted() return s[min(s.count - 1, Int(Double(s.count) * p))] } func floatsData(_ v: [Float]) -> Data { v.withUnsafeBufferPointer { Data(buffer: $0) } } func readFloats(_ url: URL) throws -> [Float] { let d = try Data(contentsOf: url) return d.withUnsafeBytes { Array($0.bindMemory(to: Float.self)) } } guard !args.isEmpty else { usage() } let cmd = args.removeFirst() let cleanup = takeFlag("--cleanup-temp") @MainActor func computeOption(_ name: String) -> ComputePreference? { guard let raw = takeOption(name) else { return nil } guard let c = ComputePreference(rawValue: raw) else { print("\(name) must be ane, gpu, cpu or default") exit(2) } return c } let compute = computeOption("--compute") ?? .neuralEngine let imageCompute = computeOption("--image-compute") // nil = same as --compute let audioCompute = computeOption("--audio-compute") // nil = same as --compute let exif = takeFlag("--exif") let textOnly = takeFlag("--text-only") let longPolicyRaw = takeOption("--long") ?? "truncate" guard let longPolicy = LongAudioPolicy(rawValue: longPolicyRaw) else { print("--long must be truncate or chunk_mean") exit(2) } @MainActor func finish(_ code: Int32) -> Never { if cleanup { let r = EmbeddingGemma2.removeOwnMPSGraphTempFiles() print("cleanup-temp: removed \(r.folders) MPSGraph temp folder(s), \(fmt(Double(r.bytes) / 1048576, 1)) MB") } exit(code) } if cmd == "cleanup-temp" { finish(0) } struct PixCase: Decodable { let file: String; let width: Int; let height: Int; let resized_width: Int; let resized_height: Int; let n_soft: Int let decoded_sha256: String; let resized_sha256: String; let lossy: Bool } /// One entry of tests/audio-preprocess-cases.json: the Python front end's result on a shipped clip, with the /// 16 kHz waveform and the log-mel features stored next to it as raw float32 files. struct AudioCase: Decodable { let file: String; let sample_rate: Int; let channels: Int; let n_samples16k: Int; let n_frames: Int; let n_tokens: Int let bucket: Int; let wave_file: String; let features_file: String let wave_tolerance: Float; let feature_tolerance: Float; let note: String? } // Smoke reference files: text (v1.0 format, "cases"), images ("images" + "texts") or audio ("audio" + "texts"). struct TextCase: Decodable { let text: String; let kind: String; let embedding: [Float] } struct ImageCase: Decodable { let file: String; let query: String; let embedding: [Float] } struct AudioRefCase: Decodable { let file: String; let query: String?; let embedding: [Float]; let threshold_cosine: Double?; let fp32_rank1: Bool? } struct RefFile: Decodable { let threshold_cosine: Double let threshold_cosine_image: Double? let threshold_cosine_audio: Double? let cases: [TextCase]? let images: [ImageCase]? let audio: [AudioRefCase]? let texts: [TextCase]? } do { switch cmd { case "pixcheck": guard let casesPath = args.first else { usage() } let url = URL(fileURLWithPath: casesPath) let cases = try JSONDecoder().decode([PixCase].self, from: Data(contentsOf: url)) let base = url.deletingLastPathComponent() var exact = 0, lossyDiff = 0, fail = 0 let t0 = Date() for c in cases { let img = try ImageDecoder.decodeRGB(url: base.appendingPathComponent(c.file), applyEXIFOrientation: false) let r = try ImagePreprocessor.resized(img) let inputs = ImagePreprocessor.patchify(resizedRGB: r.rgb, width: r.width, height: r.height) let dims = img.width == c.width && img.height == c.height && r.width == c.resized_width && r.height == c.resized_height && inputs.softTokenCount == c.n_soft let dOK = sha256(img.rgb) == c.decoded_sha256 let rOK = sha256(r.rgb) == c.resized_sha256 if dims && dOK && rOK { exact += 1 print("EXACT \(c.file) \(img.width)x\(img.height) -> \(r.width)x\(r.height), n=\(inputs.softTokenCount)") } else if dims && c.lossy && !dOK { lossyDiff += 1 print("DIFFERS \(c.file) (lossy format: decoder differences are expected; resized \(rOK ? "identical" : "differs"))") } else { fail += 1 print("FAIL \(c.file) dims \(dims) decoded \(dOK) resized \(rOK) (\(img.width)x\(img.height) -> \(r.width)x\(r.height), n=\(inputs.softTokenCount))") } } print("PIXCHECK \(exact)/\(cases.count) bit-identical to the Python reference, \(lossyDiff) lossy-format decoder differences, \(fail) failures, \(fmt(Date().timeIntervalSince(t0), 2)) s") finish(fail == 0 ? 0 : 1) case "audiocheck": guard let casesPath = args.first else { usage() } let url = URL(fileURLWithPath: casesPath) let cases = try JSONDecoder().decode([AudioCase].self, from: Data(contentsOf: url)) let base = url.deletingLastPathComponent() var pass = 0, fail = 0, waveExact = 0 var worstWave: Float = 0, worstFeat: Float = 0 let t0 = Date() for c in cases { let t1 = Date() let d = try AudioDecoder.decode(url: base.appendingPathComponent(c.file)) let wav = AudioResampler.resampleTo16k(AudioFrontend.toMono(d), rate: d.sampleRate) let inputs = try AudioFrontend.prepare(wav16k: wav) let ms = Date().timeIntervalSince(t1) * 1000 let refWave = try readFloats(base.appendingPathComponent(c.wave_file)) let refFeat = try readFloats(base.appendingPathComponent(c.features_file)) let counts = d.sampleRate == c.sample_rate && d.channelCount == c.channels && wav.count == c.n_samples16k && inputs.frameCount == c.n_frames && inputs.softTokenCount == c.n_tokens && inputs.bucket == c.bucket var wDiff: Float = 0 if wav.count == refWave.count { for i in 0.. \(wav.count) samples, \(inputs.frameCount) frames, \(inputs.softTokenCount) tokens (bucket \(inputs.bucket)); " + "wave max diff \(wDiff == 0 ? "0 (exact)" : String(format: "%.2e", wDiff)) (tol \(String(format: "%.0e", c.wave_tolerance))), " + "features max diff \(String(format: "%.2e", fDiff)) (tol \(String(format: "%.0e", c.feature_tolerance))), \(fmt(ms, 0)) ms\(counts ? "" : " COUNTS DIFFER")") } print("AUDIOCHECK \(pass)/\(cases.count) within tolerance of the Python reference (\(waveExact) waveforms bit-identical; worst wave diff \(String(format: "%.2e", worstWave)), worst feature diff \(String(format: "%.2e", worstFeat))), \(fail) failures, \(fmt(Date().timeIntervalSince(t0), 2)) s") finish(fail == 0 ? 0 : 1) case "dump": guard args.count >= 2 else { usage() } let img = try ImageDecoder.decodeRGB(url: URL(fileURLWithPath: args[0]), applyEXIFOrientation: exif) let r = try ImagePreprocessor.resized(img) let inputs = ImagePreprocessor.patchify(resizedRGB: r.rgb, width: r.width, height: r.height) try Data(img.rgb).write(to: URL(fileURLWithPath: args[1] + ".decoded.rgb")) try Data(r.rgb).write(to: URL(fileURLWithPath: args[1] + ".resized.rgb")) let info: [String: Any] = ["width": img.width, "height": img.height, "resized_width": r.width, "resized_height": r.height, "n_soft": inputs.softTokenCount, "decoded_sha256": sha256(img.rgb), "resized_sha256": sha256(r.rgb)] try JSONSerialization.data(withJSONObject: info).write(to: URL(fileURLWithPath: args[1] + ".json")) print(String(decoding: try JSONSerialization.data(withJSONObject: info), as: UTF8.self)) finish(0) case "dump-audio": guard args.count >= 2 else { usage() } let d = try AudioDecoder.decode(url: URL(fileURLWithPath: args[0])) let wav = AudioResampler.resampleTo16k(AudioFrontend.toMono(d), rate: d.sampleRate) let (feats, frames) = AudioFrontend.logMel(wav) try floatsData(wav).write(to: URL(fileURLWithPath: args[1] + ".wave16k.f32")) try floatsData(feats).write(to: URL(fileURLWithPath: args[1] + ".features.f32")) let info: [String: Any] = ["sample_rate": d.sampleRate, "channels": d.channelCount, "decoded_frames": d.frameCount, "n_samples16k": wav.count, "n_frames": frames, "n_tokens": AudioFrontend.softTokenCount(frames: frames)] try JSONSerialization.data(withJSONObject: info).write(to: URL(fileURLWithPath: args[1] + ".json")) print(String(decoding: try JSONSerialization.data(withJSONObject: info), as: UTF8.self)) finish(0) default: break } guard let bundlePath = args.first else { usage() } args.removeFirst() let bundle = URL(fileURLWithPath: bundlePath) switch cmd { case "embed": let kind = takeOption("--kind") ?? "document" let dim = Int(takeOption("--dim") ?? "768") ?? 768 let eg2 = try await EmbeddingGemma2(bundle: bundle, compute: compute) for text in args { let v = try await eg2.embed(text, kind: kind == "none" ? nil : kind, dim: dim) let json: [String: Any] = ["text": text, "kind": kind, "dim": dim, "embedding": v.map { Double($0) }] print(String(decoding: try JSONSerialization.data(withJSONObject: json), as: UTF8.self)) } _ = consume eg2 finish(0) case "embed-image": let dim = Int(takeOption("--dim") ?? "768") ?? 768 let eg2 = try await EmbeddingGemma2(bundle: bundle, compute: compute, loadVision: true, imageCompute: imageCompute) for path in args { let t0 = Date() let inputs = try ImagePreprocessor.prepare(url: URL(fileURLWithPath: path), applyEXIFOrientation: exif) let t1 = Date() let v = try await eg2.embedImage(inputs, dim: dim) let json: [String: Any] = ["image": path, "dim": dim, "n_soft": inputs.softTokenCount, "resized": [inputs.resizedWidth, inputs.resizedHeight], "exif_orientation_applied": exif, "preprocess_ms": Int(t1.timeIntervalSince(t0) * 1000), "model_ms": Int(Date().timeIntervalSince(t1) * 1000), "embedding": v.map { Double($0) }] print(String(decoding: try JSONSerialization.data(withJSONObject: json), as: UTF8.self)) } _ = consume eg2 finish(0) case "embed-audio": let dim = Int(takeOption("--dim") ?? "768") ?? 768 let eg2 = try await EmbeddingGemma2(bundle: bundle, compute: compute, loadAudio: true, audioCompute: audioCompute) for path in args { let t0 = Date() let d = try AudioDecoder.decode(url: URL(fileURLWithPath: path)) let wav = AudioResampler.resampleTo16k(AudioFrontend.toMono(d), rate: d.sampleRate) let t1 = Date() let v = try await eg2.embedAudio(samples16k: wav, dim: dim, longAudio: longPolicy) let frames = AudioFrontend.validFrameCount(samples: wav.count) let json: [String: Any] = ["audio": path, "dim": dim, "sample_rate": d.sampleRate, "channels": d.channelCount, "seconds": Double(wav.count) / 16000.0, "n_frames": frames, "n_soft": AudioFrontend.softTokenCount(frames: frames), "truncated_to_30s": wav.count > AudioFrontend.maxSamples && longPolicy == .truncate, "long_audio": longPolicy.rawValue, "decode_ms": Int(t1.timeIntervalSince(t0) * 1000), "model_ms": Int(Date().timeIntervalSince(t1) * 1000), "embedding": v.map { Double($0) }] print(String(decoding: try JSONSerialization.data(withJSONObject: json), as: UTF8.self)) } _ = consume eg2 finish(0) case "smoke": guard !args.isEmpty else { usage() } let refs = try args.map { path -> (URL, RefFile) in let url = URL(fileURLWithPath: path) return (url, try JSONDecoder().decode(RefFile.self, from: Data(contentsOf: url))) } let needsVision = refs.contains { !($0.1.images ?? []).isEmpty } let needsAudio = refs.contains { !($0.1.audio ?? []).isEmpty } let eg2 = try await EmbeddingGemma2(bundle: bundle, compute: compute, loadVision: needsVision, imageCompute: imageCompute, loadAudio: needsAudio, audioCompute: audioCompute) print("loaded in \(fmt(eg2.loadSeconds, 1)) s (tokenizer \(fmt(eg2.tokenizerLoadSeconds, 2)) s), compute=\(compute.rawValue), " + "image-compute=\(eg2.hasVision ? eg2.imageCompute.rawValue : "-"), audio-compute=\(eg2.hasAudio ? eg2.audioCompute.rawValue : "-"), " + "buckets=\(eg2.buckets), vision \(eg2.hasVision ? "loaded" : "not loaded"), audio \(eg2.hasAudio ? "loaded" : "not loaded")") var ok = true for (refURL, ref) in refs { print("-- \(refURL.lastPathComponent)") // text cases (v1.0 format) for c in ref.cases ?? [] { let v = try await eg2.embed(c.text, kind: c.kind) let cos = cosineSimilarity(v, c.embedding) let pass = cos >= ref.threshold_cosine && v.allSatisfy { $0.isFinite } ok = ok && pass print("\(pass ? "PASS" : "FAIL") cosine \(fmt(cos, 6)) (threshold \(ref.threshold_cosine)) \(c.kind): \(c.text.prefix(60))") } if let first = ref.cases?.dropFirst().first { // Matryoshka prefixes must stay close to the truncated + re-normalized reference for dim in [512, 256, 128] { let v = try await eg2.embed(first.text, kind: first.kind, dim: dim) let cos = cosineSimilarity(v, Array(first.embedding.prefix(dim))) let pass = cos >= ref.threshold_cosine ok = ok && pass print("\(pass ? "PASS" : "FAIL") cosine \(fmt(cos, 6)) at dim \(dim)") } } // images, their queries and documents let images = ref.images ?? [] var imgVecs: [[Float]] = [] let imgThreshold = ref.threshold_cosine_image ?? ref.threshold_cosine for c in images { let t0 = Date() let v = try await eg2.embedImage(url: refURL.deletingLastPathComponent().appendingPathComponent(c.file)) let cos = cosineSimilarity(v, c.embedding) let pass = cos >= imgThreshold && v.allSatisfy { $0.isFinite } ok = ok && pass imgVecs.append(v) print("\(pass ? "PASS" : "FAIL") cosine \(fmt(cos, 6)) (threshold \(imgThreshold)) image \(c.file) \(Int(Date().timeIntervalSince(t0) * 1000)) ms") } // audio clips (per-clip thresholds: resampled and lossy files are allowed more) let audio = ref.audio ?? [] var audVecs: [[Float]] = [] for c in audio { let t0 = Date() let v = try await eg2.embedAudio(url: refURL.deletingLastPathComponent().appendingPathComponent(c.file)) let cos = cosineSimilarity(v, c.embedding) let thr = c.threshold_cosine ?? ref.threshold_cosine_audio ?? ref.threshold_cosine let pass = cos >= thr && v.allSatisfy { $0.isFinite } ok = ok && pass audVecs.append(v) print("\(pass ? "PASS" : "FAIL") cosine \(fmt(cos, 6)) (threshold \(thr)) audio \(c.file) \(Int(Date().timeIntervalSince(t0) * 1000)) ms") } var queryVecs: [(String, [Float])] = [] for c in ref.texts ?? [] { let v = try await eg2.embed(c.text, kind: c.kind) let cos = cosineSimilarity(v, c.embedding) let pass = cos >= ref.threshold_cosine && v.allSatisfy { $0.isFinite } ok = ok && pass if c.kind == "search_query" { queryVecs.append((c.text, v)) } print("\(pass ? "PASS" : "FAIL") cosine \(fmt(cos, 6)) (threshold \(ref.threshold_cosine)) \(c.kind): \(c.text.prefix(60))") } // text -> image: each query must rank its own image first if !images.isEmpty { var hits = 0 for (q, qv) in queryVecs { let scores = imgVecs.map { cosineSimilarity(qv, $0) } let best = scores.indices.max(by: { scores[$0] < scores[$1] })! let hit = images.firstIndex(where: { $0.query == q }) == best hits += hit ? 1 : 0 print("\(hit ? "PASS" : "FAIL") query \"\(q)\" -> \(images[best].file) (\(fmt(scores[best], 3)))") } ok = ok && hits == queryVecs.count print("text -> image: \(hits)/\(queryVecs.count) queries rank their own image first") } // text -> audio: each query must rank its own clip first (among the clips that have a query), checked only // where the fp32 reference itself ranks it first (fp32_rank1; synthesized sounds are a hard retrieval case) if !audio.isEmpty { let withQuery = audio.indices.filter { audio[$0].query != nil } var hits = 0, total = 0 for (q, qv) in queryVecs { guard let want = audio.firstIndex(where: { $0.query == q }) else { continue } let scores = withQuery.map { cosineSimilarity(qv, audVecs[$0]) } let best = withQuery[scores.indices.max(by: { scores[$0] < scores[$1] })!] let hit = best == want if audio[want].fp32_rank1 == false { print("SKIP query \"\(q)\" -> \(audio[best].file) (\(fmt(scores.max()!, 3))) (fp32 does not rank this clip first either)") continue } total += 1 hits += hit ? 1 : 0 print("\(hit ? "PASS" : "FAIL") query \"\(q)\" -> \(audio[best].file) (\(fmt(scores.max()!, 3)))") } ok = ok && hits == total print("text -> audio: \(hits)/\(total) queries rank their own clip first (as fp32 does)") } } let fp = footprints() print("peak footprint \(fmt(fp.physMB, 0)) MB, neural footprint \(fmt(fp.neuralMB, 0)) MB") print(ok ? "SMOKE PASS" : "SMOKE FAIL") _ = consume eg2 finish(ok ? 0 : 1) case "tokcheck": guard let casesPath = args.first else { usage() } struct Case: Decodable { let cat: String; let kind: String; let text: String; let ids: [Int32] } let cases = try JSONDecoder().decode([Case].self, from: Data(contentsOf: URL(fileURLWithPath: casesPath))) let config = try JSONDecoder().decode(BundleConfig.self, from: Data(contentsOf: bundle.appendingPathComponent("config.json"))) let t0 = Date() let tok = try GemmaTokenizer(contentsOf: bundle.appendingPathComponent("tokenizer/tokenizer.json")) print("tokenizer loaded in \(fmt(Date().timeIntervalSince(t0), 2)) s, vocab \(tok.vocabSize)") let prefixes = ["q": "task: search result | query: ", "d": "title: none | text: ", "raw": ""] var same = 0, totalIDs = 0, shown = 0 var perCat: [String: (Int, Int)] = [:] let t1 = Date() let cpu0 = cpuSeconds() for c in cases { let ids = tok.encodeWithSpecials(prefixes[c.kind]! + c.text) let ok = ids == c.ids totalIDs += c.ids.count var e = perCat[c.cat] ?? (0, 0) e.1 += 1 if ok { same += 1; e.0 += 1 } else if shown < 10 { shown += 1 print("DIFFERENT [\(c.cat)] \(c.text.prefix(60).debugDescription): got \(ids.prefix(12)) expected \(c.ids.prefix(12))") } perCat[c.cat] = e } let secs = Date().timeIntervalSince(t1), cpu = cpuSeconds() - cpu0 for (cat, e) in perCat.sorted(by: { $0.key < $1.key }) { print(" \(cat): \(e.0)/\(e.1)") } print("TOKCHECK \(same)/\(cases.count) texts identical to Hugging Face tokenizers (\(totalIDs) ids) in \(fmt(secs, 2)) s wall / \(fmt(cpu, 2)) s CPU") print("config bos/eos/pad \(config.bos_token_id)/\(config.eos_token_id)/\(config.pad_token_id) == tokenizer \(tok.bosID)/\(tok.eosID)/\(tok.padID)") finish(same == cases.count ? 0 : 1) case "latency": let trials = Int(takeOption("--trials") ?? "50") ?? 50 let hasAudioAsset = (try? JSONDecoder().decode(BundleConfig.self, from: Data(contentsOf: bundle.appendingPathComponent("config.json"))))?.audio != nil let eg2 = try await EmbeddingGemma2(bundle: bundle, compute: compute, loadVision: !textOnly, imageCompute: imageCompute, loadAudio: !textOnly && hasAudioAsset, audioCompute: audioCompute) print("loaded in \(fmt(eg2.loadSeconds, 1)) s, compute=\(compute.rawValue), image-compute=\(eg2.hasVision ? eg2.imageCompute.rawValue : "-"), " + "audio-compute=\(eg2.hasAudio ? eg2.audioCompute.rawValue : "-"), vision \(eg2.hasVision ? "loaded" : "not loaded"), audio \(eg2.hasAudio ? "loaded" : "not loaded")") let warm = try await eg2.prewarm() print("prewarm \(fmt(warm, 2)) s") if eg2.hasVision { // a synthetic 1008x624 image (273 soft tokens, the most common shape) var px = [UInt8](repeating: 0, count: 1008 * 624 * 3) for i in px.indices { px[i] = UInt8((i * 7919) % 251) } let inputs = ImagePreprocessor.patchify(resizedRGB: px, width: 1008, height: 624) var tv: [Double] = [], tt: [Double] = [], te: [Double] = [] let imageTrials = max(5, trials / 2) let cpu0 = cpuSeconds() for _ in 0.. 1008x624 + patches): \(fmt(Date().timeIntervalSince(tp0) / 5 * 1000, 1)) ms") } if eg2.hasAudio { // synthetic clips of 3 s, 10 s and 30 s (a sum of tones and noise), per frame bucket let audioTrials = max(5, trials / 2) print("audio seconds bucket tokens audio_ p50 ms mm_ p50 ms end-to-end p50 ms CPU ms/clip front end ms") for seconds in [3.0, 10.0, 30.0] { let n = Int(seconds * 16000) var wav = [Float](repeating: 0, count: n) for i in 0.. 0 ? "Neural Engine in use" : "Neural Engine NOT used"))") _ = consume eg2 finish(0) case "prewarm": let t0 = Date() let hasAudioAsset = (try? JSONDecoder().decode(BundleConfig.self, from: Data(contentsOf: bundle.appendingPathComponent("config.json"))))?.audio != nil let eg2 = try await EmbeddingGemma2(bundle: bundle, compute: compute, loadVision: !textOnly, imageCompute: imageCompute, loadAudio: !textOnly && hasAudioAsset, audioCompute: audioCompute) let warm = try await eg2.prewarm() print("load \(fmt(eg2.loadSeconds, 1)) s + prewarm \(fmt(warm, 2)) s = ready in \(fmt(Date().timeIntervalSince(t0), 1)) s " + "(vision \(eg2.hasVision ? "loaded" : "not loaded"), audio \(eg2.hasAudio ? "loaded" : "not loaded"))") _ = consume eg2 finish(0) default: usage() } } catch { print("error: \(error)") finish(1) }