Verdure

For developers

Put Verdure inside your own app.

Speech to text, and text to a filled form, on your user’s own device with no internet. This page is the whole story: what the runtime does, the code you write, how it chooses a model, and the numbers we measured, including the ones that are not good enough yet.

What it is

Verdure is a runtime a company embeds in its own app. On the user’s device, with no internet, it does three things:

  1. Speech to text. Microphone audio in, transcript events out.
  2. Text to a filled form. You give it free text and a JSON Schema you wrote; it returns JSON that matches the schema, or an error that says exactly which fields were wrong.
  3. Search over documents already on the device. You index documents; you ask a question; you get the best passages back.

The company keeps its own app, screens and cloud. Verdure is the engine underneath. It never opens a network connection except to download model files from a server the company configured, and only when the company’s policy allows that.

It is written in Rust and ships as one library with whisper.cpp and llama.cpp compiled in, with bindings for Kotlin, Python and Rust today. The licence is a signed token verified offline. The models come from a signed manifest the company writes: Verdure never adds a model that is not listed.

How it works

THE USER'S DEVICE · PHONE OR LAPTOP · NO INTERNET NEEDEDSpeech16 kHz audio, any language hintVerdure runtimepicks the model this device can runwhisper.cpp → transcriptllama.cpp → JSON that matches your schemaone library · one licence token · offlineTranscriptas the person speaks{ "facts": [...] }valid against your schemaNothing crosses the dotted line. Model files arrive once, from a server you choose.

The flow your code drives, in order:

  1. start — verify the licence token, read the device: tier, free memory, free disk, cores. No network.
  2. loadManifest — trust the signed list of bundles. A bundle is one set of model files for one task on certain device tiers.
  3. recommend — the bundle this device can run now, for live speech. Everything passed over is explained in a sentence: not enough memory, not enough disk, too slow to keep pace.
  4. download — every file, resumable, verified by SHA-256, shared between bundles that use the same model.
  5. load — the engines into memory. Refused with the numbers if memory is short.
  6. transcribe — push 16 kHz audio, get transcript events.
  7. extract — the transcript plus your schema in, valid JSON out. The model only ever sees an outline of the schema and the rule “never invent numbers, names or dates”; the output is validated against the real schema, and one repair round is attempted before an error is returned.

Hello world

The same flow in each language. The Kotlin one is what an Android app calls; it compiled and ran unchanged when we swapped a new model under a test integration, because the app never names a model.

import uniffi.verdure.*

val sdk = Verdure.start(token, PolicyConfig(networkDownloads = true, allowedHosts = emptyList(),
                        localCounters = false, encryptedStorage = true, retentionDays = null),
                        filesDir.resolve("verdure").absolutePath)
sdk.loadManifest(manifestJson, signature, publicKey)

val bundle = sdk.recommend(Task.SCRIBE)        // what this phone can run live; bundle.skipped says why not the rest
if (!sdk.isDownloaded(bundle.id)) sdk.download(bundle.id, listener)
sdk.load(bundle.id)

val stream = sdk.transcribe(AsrConfig(langHints = listOf("en"), keyterms = listOf(), replacements = listOf()))
for (ev in stream.pushPcm(floats, 16000u))     // 16 kHz mono from AudioRecord, as floats in [-1, 1]
    if (ev is TranscriptEvent.Final) show(ev.text)

val facts = sdk.extract(transcript, schemaJson).json   // JSON that validates against YOUR schema

Leave keyterms empty: a 40-word clinical list made recognition worse in our measurements. Use replacements for spellings you need fixed. Every call blocks; keep them off the main thread.

The API

Every method on Verdure. Kotlin spelling shown; Rust and Python use snake_case.

MethodDoesFails with
start(token, policy, dataDir)Verify the licence offline, read the device. No network, instant.License
capabilities()Tier, free memory and disk, cores, GPU class, engines built, model attributions, notes.—
loadManifest(json, signature, publicKey)Trust a signed manifest you ship inside the app — the fully offline path.Integrity
setBundleSource(url, publicKey)Fetch and verify a signed manifest from your host instead.Policy, Network, Integrity
recommend(task)The bundle that fits this device now and keeps pace with live speech where we measured it. Says what it skipped and why.NoBundle
recommendFor(task, pace)The same with the pace stated: Pace.RECORDED allows a slower, better bundle for audio already captured.NoBundle
fit(bundleId)Judge a bundle you chose yourself: fits, plus every problem as a sentence. Nothing is substituted.NoBundle
fitFor(bundleId, pace)The same for the pace stated.NoBundle
download(bundleId, listener)Download or resume every file, verified by hash, reusing any model another bundle already holds. Bytes and one sentence per file to the listener.License, Policy, Network, Integrity, Cancelled
isDownloaded(bundleId)All files present and their hashes match.—
load(bundleId)Load the engines. Refuses with the numbers when memory is short. 1.2 s for the base bundle, 15 s for the small one on a Pixel 8a.Memory, EngineUnavailable, Engine
loadOvercommit(bundleId)load without the memory gate, for a device you have measured yourself.as load
transcribe(config)Open a stream: pushPcm(samples, rate) returns transcript events; finish() flushes.EngineUnavailable
extract(text, schemaJson)JSON valid against your schema, or an error naming each field that was wrong. Blocks for the whole generation.EngineUnavailable, InvalidSchema, SchemaInvalid
chat(messages, sink)One conversation turn, streamed token by token.EngineUnavailable, Engine
index(collection, docs)Chunk and index documents already on the device.Storage
query(collection, question, k)The best k passages.Storage
delete(scope)A collection, the indexes, or everything — the encryption key first, then the files.Storage
setCloudFallback(hook)Give Verdure your cloud for when no engine is loaded. Verdure never opens that socket itself.—

Errors are typed and their messages are sentences for your log: SchemaInvalid lists each violation by JSON path; NoBundle says what did not fit and why. Show your user a sentence of your own.

Which model, which device

Verdure sorts devices into six tiers by what they can honestly hold, and a bundle declares which tiers it is built for. The device has the last word: recommend reads free memory right now, not a spec sheet.

TierDeviceFor example
P14 GB phoneItel A70, Tecno Spark Go
P26 GB phoneRedmi 13C, Galaxy A16
P38 GB phonePixel 8a, iPhone 12 and later
L14 GB laptopThinkPad T460
L28 GB laptop2019 Intel MacBook, a school-lab PC
L316 GB+ laptopApple Silicon, or a discrete GPU

Memory is a rule, not a guess: a language model needs its file size times 1.5 plus 400 MB, the speech model its size plus 100 MB. Speed is a measurement the manifest carries per tier. For live speech, a bundle slower than 1.5× real time on this tier is passed over with the number as the reason; a tier we have not measured is not judged. For audio that is already recorded, recommendFor(task, Pace.RECORDED) allows the slower, better bundle.

You can always pin a bundle yourself: fit(id) tells you whether it fits that device and at what speed, and loadOvercommit exists for a device you have measured yourself.

What we measured

Ten real two-speaker GP consultations (PriMock57, 12,000 reference words), scored on word error rate and on a list of a hundred clinical terms, run through the SDK’s own transcribe() on an 8 GB Intel Mac and on a Pixel 8a inside a test Android app. Speed is processing time per second of speech, fed flat out; the phone figure is the sustained, throttled one.

Speech modelBundleDownloadWord errorClinical terms missedSpeed, Intel MacSpeed, Pixel 8a
Whisper basescribe-floor148 MB26.5 %1 in 3.60.28×1.25×
Whisper smallscribe-small488 MB21.6 %1 in 60.75×3.2×
  • Quality travels. The phone produced the same transcript quality as the laptop, file for file, with the same words missed.
  • Memory on the Pixel 8a: the base bundle 683 MB idle and 952 MB peak; the small bundle 635 MB idle and 1,143 MB peak. No out-of-memory kill in 79 minutes of audio.
  • Download and load: 979 MB in 54 s over Wi-Fi, verified; load 15 s (small) and 1.2 s (base). Two bundles that share a model download it once.
  • Keyterms hurt. Feeding a 40-word vocabulary list to the recogniser doubled the error rate. It is documented as experimental and off by default.
  • A medical fine-tune was worse. A clinical Whisper medium fine-tuned on African-accented read speech scored 83 % word error on these consultations as converted. We do not ship it.

Full tables and method in the repository’s TEST_REPORT.md and knowledge/26-sdk.md; they come with the SDK.

Privacy and licences

What leaves the device: nothing, except the model files coming in, from a host you name in the policy, over https only. No telemetry, no accounts, no counters unless you turn local counters on for yourself. Storage is encrypted by default and delete(All) destroys the key before the files. Verdure holds no user data on any server, so it is not a processor of it.

The licence token is verified offline against a public key; a token names the vendor, the seats, the bundles and the model licences the vendor has acknowledged. Those model licences are listed, not discovered:

ComponentLicenceFrom
Whisper (base, small) weightsMITOpenAI
whisper.cpp, llama.cpp (the engines)MITggml
Qwen2.5 0.5B and 1.5B InstructApache-2.0Alibaba Qwen

All permissive and shippable in a commercial product. A bundle whose model needs an acknowledgement (Llama, Gemma terms) is refused until the token carries it; attribution strings a licence requires are handed to your app by capabilities().

What is not done yet

  • Extraction on the smallest language model is not good enough. The 0.5B model returned schema-valid facts on 3 of 10 one-minute windows on the phone; the rest failed validation. The 1.5B bundle is the next measurement. Until then, extraction on a phone is a demonstration, not a product.
  • Speed is measured on two devices. A Pixel 8a and an 8 GB Intel Mac. Other tiers are not judged for speed until we measure them.
  • Windows are fixed at 12 seconds. Sentences get cut; voice-activity windowing is next.
  • Grammar-constrained decoding is built but off, after the engine aborted on a multi-byte token in an earlier app. Validation plus one repair round stands in for it.
  • Bindings: Kotlin, Python and Rust today. Swift and a C header are generated by the same tool and not yet packaged.

Get the test kit

Write to us and say which platform you build for. You receive the Android zip (library, Kotlin binding, a ten-year sandbox licence, a signed example manifest with real models, the guide and a changelog) or the repository for Rust and Python, and a person who answers questions the same day.