CactusBrain Text
Embeddings, ranking and chat that run on the device. Same package, same project key and same model delivery as Vision. Version 0.1.0.
What it does
- Embeddings. Turn text into vectors for search, clustering and deduplication without sending anything to a server.
- Ranking. Score documents against a query, optionally personalized to a single user’s behaviour on their own device.
- Chat. Streamed replies from a small local model, with two sizes:
compactfor the default case andcapablewhen instruction-following matters more than footprint. - Translation. Between the languages listed under Translation languages, on the device, with no per-request call. iOS only today.
Text stores installed models in the same application-private directory as Vision, so a device holds one index rather than two that can disagree about what is on disk.
Install on iOS
// Package.swift
dependencies: [
.package(url: "https://github.com/cactusbrain/cactusbrain-swift", from: "0.1.0")
]
// Then add the product to your target:
.product(name: "CactusBrainTextSDK", package: "cactusbrain-swift")CactusBrainText is a separate product in the same package, so adding it does not pull in the vision models or their code.
Configure and chat
import CactusBrainTextSDK
let session = try await CactusBrainTextSDK.configure(
projectKey: Secrets.cactusBrainProjectKey
)// Streams text pieces, not tokens. Append them in order.
var reply = ""
for try await piece in try await session.chat("Summarize this in one line.") {
reply += piece
}
// A truncated reply is worth showing differently to a finished one.
if await session.stopReason() == .tokenLimit {
showTruncatedNotice()
}The model keeps no memory between calls. Pass the whole conversation as [ChatMessage] — anything you leave out did not happen as far as the reply is concerned.
// Embeddings for search, clustering or ranking. All local.
let vector = try await session.embed("flat white, oat milk")
let ranked = try await session.personalizer(...)
.rank(query: "coffee order", limit: 5)Install on Android
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
// build.gradle.kts
dependencies {
implementation("ai.cactusbrain:cactusbrain-text:0.1.0")
}import ai.cactusbrain.text.CactusBrainText
val session = CactusBrainText.configure(
context = applicationContext,
projectKey = BuildConfig.CACTUSBRAIN_PROJECT_KEY,
)// A cold flow: collecting starts the turn, cancelling ends it.
val reply = StringBuilder()
session.chat("Summarize this in one line.")
.collect { piece -> reply.append(piece) }
if (session.stopReason() == ChatStopReason.TOKEN_LIMIT) {
showTruncatedNotice()
}chat returns a cold Flow<String>. Collecting it starts the turn and cancelling collection ends it after at most one more token.
Prepare before first use
Chat models are considerably larger than the vision models. Calling chat on a device that has not installed one will download it first, which is a poor first impression inside a message send.
// Chat models are several hundred megabytes. Prepare where you can
// show progress, rather than letting it happen inside the first message.
session.prepare(ChatModel.COMPACT).collect { event ->
when (event) {
is PreparationEvent.Progress -> setProgress(event.fraction)
is PreparationEvent.Installed -> enableChat()
is PreparationEvent.Failed -> showFailure(event.cause)
}
}Preparation is the same delivery path documented for Vision: the server decides entitlement, the artifact is verified before install, and a model that fails verification is discarded rather than installed.
Translation languages
Translation runs from a separate model to chat, and there is more than one: they differ in what they can say rather than how well they say it. Prepare the one you need the way you would prepare a chat model.
// Ask the server what the model addresses rather than shipping a list:
// the codes come from the tokenizer, so a picker built from them cannot
// offer a language the weights will not accept.
let offered = try await session.languages()
let twi = try await session.translate(
"Good morning",
from: .english,
to: .twi,
using: .englishToTwi
)Translate English to Twi 418M
translate-en-twi-418m — 100 language codes.
English -> Twi only. This fine-tune ignores the target language token: every code yields Twi. Use --src-lang __en__ --tgt-lang __en__.
All 100 codes for translate-en-twi-418m
afAfrikaansamAmharicarArabicastAsturianazAzerbaijanibaBashkirbeBelarusianbgBulgarianbnBanglabrBretonbsBosniancaCatalancebCebuanocsCzechcyWelshdaDanishdeGermanelGreekenEnglishesSpanishetEstonianfaPersianffFulafiFinnishfrFrenchfyWestern FrisiangaIrishgdScottish GaelicglGalicianguGujaratihaHausaheHebrewhiHindihrCroatianhtHaitian CreolehuHungarianhyArmenianidIndonesianigIgboiloIlokoisIcelandicitItalianjaJapanesejvJavanesekaGeorgiankkKazakhkmKhmerknKannadakoKoreanlbLuxembourgishlgGandalnLingalaloLaoltLithuanianlvLatvianmgMalagasymkMacedonianmlMalayalammnMongolianmrMarathimsMalaymyBurmeseneNepalinlDutchnoNorwegiannsNorthern SothoocOccitanorOdiapaPunjabiplPolishpsPashtoptPortugueseroRomanianruRussiansdSindhisiSinhalaskSlovakslSloveniansoSomalisqAlbaniansrSerbianssSwatisuSundanesesvSwedishswSwahilitaTamilthThaitlFilipinotnTswanatrTurkishukUkrainianurUrduuzUzbekviVietnamesewoWolofxhXhosayiYiddishyoYorubazhChinesezuZulu
Translate Multilingual 418M
translate-multi-418m — 100 language codes.
All 100 codes for translate-multi-418m
afAfrikaansamAmharicarArabicastAsturianazAzerbaijanibaBashkirbeBelarusianbgBulgarianbnBanglabrBretonbsBosniancaCatalancebCebuanocsCzechcyWelshdaDanishdeGermanelGreekenEnglishesSpanishetEstonianfaPersianffFulafiFinnishfrFrenchfyWestern FrisiangaIrishgdScottish GaelicglGalicianguGujaratihaHausaheHebrewhiHindihrCroatianhtHaitian CreolehuHungarianhyArmenianidIndonesianigIgboiloIlokoisIcelandicitItalianjaJapanesejvJavanesekaGeorgiankkKazakhkmKhmerknKannadakoKoreanlbLuxembourgishlgGandalnLingalaloLaoltLithuanianlvLatvianmgMalagasymkMacedonianmlMalayalammnMongolianmrMarathimsMalaymyBurmeseneNepalinlDutchnoNorwegiannsNorthern SothoocOccitanorOdiapaPunjabiplPolishpsPashtoptPortugueseroRomanianruRussiansdSindhisiSinhalaskSlovakslSloveniansoSomalisqAlbaniansrSerbianssSwatisuSundanesesvSwedishswSwahilitaTamilthThaitlFilipinotnTswanatrTurkishukUkrainianurUrduuzUzbekviVietnamesewoWolofxhXhosayiYiddishyoYorubazhChinesezuZulu
Translate Twi to English 418M
translate-twi-en-418m — 100 language codes.
Twi -> English only. Twi has no token of its own in this vocabulary, and the fine-tune ignores the codes it does have: every pair yields English. Use --src-lang __en__ --tgt-lang __en__.
All 100 codes for translate-twi-en-418m
afAfrikaansamAmharicarArabicastAsturianazAzerbaijanibaBashkirbeBelarusianbgBulgarianbnBanglabrBretonbsBosniancaCatalancebCebuanocsCzechcyWelshdaDanishdeGermanelGreekenEnglishesSpanishetEstonianfaPersianffFulafiFinnishfrFrenchfyWestern FrisiangaIrishgdScottish GaelicglGalicianguGujaratihaHausaheHebrewhiHindihrCroatianhtHaitian CreolehuHungarianhyArmenianidIndonesianigIgboiloIlokoisIcelandicitItalianjaJapanesejvJavanesekaGeorgiankkKazakhkmKhmerknKannadakoKoreanlbLuxembourgishlgGandalnLingalaloLaoltLithuanianlvLatvianmgMalagasymkMacedonianmlMalayalammnMongolianmrMarathimsMalaymyBurmeseneNepalinlDutchnoNorwegiannsNorthern SothoocOccitanorOdiapaPunjabiplPolishpsPashtoptPortugueseroRomanianruRussiansdSindhisiSinhalaskSlovakslSloveniansoSomalisqAlbaniansrSerbianssSwatisuSundanesesvSwedishswSwahilitaTamilthThaitlFilipinotnTswanatrTurkishukUkrainianurUrduuzUzbekviVietnamesewoWolofxhXhosayiYiddishyoYorubazhChinesezuZulu
These lists are read from the published artifacts, which take them from the tokenizer at packaging time — not typed here by hand. Your application should read them the same way, through session.languages(), rather than copying them into a picker that will drift.
// Some models address fewer languages than they list. Read `support`
// before rendering `codes` as a pair of pickers.
if offered.isFullyMultilingual {
showLanguagePickers(offered.codes)
} else if let note = offered.support {
showFixedDirection(note)
}
// The SDK also states this as data, which is what a UI wants:
if let only = TranslationModel.englishToTwi.directions?.first {
showFixedDirection(from: only.source, to: only.target)
}Offline and privacy
Once a model is installed, embedding, ranking and chat all run with no network. Prompts, replies and embedded text stay on the device — there is no per-message server call, so we could not read them even if we wanted to.
See Security and model delivery for how entitlement, revocation and integrity verification work. They are identical for Text and Vision.
Known limitations
- These are small models. They are useful for summarizing, rewriting, extraction and short answers — not for long-form reasoning or factual recall about the world.
- A reply that ends at
tokenLimitis truncated, not finished. RaisemaxNewTokensor show the difference in your UI. - Android ships
arm64-v8aonly. There is no x86_64 slice, so the emulator needs an arm64 image.