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.

Text

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: compact for the default case and capable when 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.
One session, one model index

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.

iOS

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.

iOS

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)
Android

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.

Both platforms

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.

iOS

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.

Reads narrower than it lists

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
  • af Afrikaans
  • am Amharic
  • ar Arabic
  • ast Asturian
  • az Azerbaijani
  • ba Bashkir
  • be Belarusian
  • bg Bulgarian
  • bn Bangla
  • br Breton
  • bs Bosnian
  • ca Catalan
  • ceb Cebuano
  • cs Czech
  • cy Welsh
  • da Danish
  • de German
  • el Greek
  • en English
  • es Spanish
  • et Estonian
  • fa Persian
  • ff Fula
  • fi Finnish
  • fr French
  • fy Western Frisian
  • ga Irish
  • gd Scottish Gaelic
  • gl Galician
  • gu Gujarati
  • ha Hausa
  • he Hebrew
  • hi Hindi
  • hr Croatian
  • ht Haitian Creole
  • hu Hungarian
  • hy Armenian
  • id Indonesian
  • ig Igbo
  • ilo Iloko
  • is Icelandic
  • it Italian
  • ja Japanese
  • jv Javanese
  • ka Georgian
  • kk Kazakh
  • km Khmer
  • kn Kannada
  • ko Korean
  • lb Luxembourgish
  • lg Ganda
  • ln Lingala
  • lo Lao
  • lt Lithuanian
  • lv Latvian
  • mg Malagasy
  • mk Macedonian
  • ml Malayalam
  • mn Mongolian
  • mr Marathi
  • ms Malay
  • my Burmese
  • ne Nepali
  • nl Dutch
  • no Norwegian
  • ns Northern Sotho
  • oc Occitan
  • or Odia
  • pa Punjabi
  • pl Polish
  • ps Pashto
  • pt Portuguese
  • ro Romanian
  • ru Russian
  • sd Sindhi
  • si Sinhala
  • sk Slovak
  • sl Slovenian
  • so Somali
  • sq Albanian
  • sr Serbian
  • ss Swati
  • su Sundanese
  • sv Swedish
  • sw Swahili
  • ta Tamil
  • th Thai
  • tl Filipino
  • tn Tswana
  • tr Turkish
  • uk Ukrainian
  • ur Urdu
  • uz Uzbek
  • vi Vietnamese
  • wo Wolof
  • xh Xhosa
  • yi Yiddish
  • yo Yoruba
  • zh Chinese
  • zu Zulu

Translate Multilingual 418M

translate-multi-418m — 100 language codes.

All 100 codes for translate-multi-418m
  • af Afrikaans
  • am Amharic
  • ar Arabic
  • ast Asturian
  • az Azerbaijani
  • ba Bashkir
  • be Belarusian
  • bg Bulgarian
  • bn Bangla
  • br Breton
  • bs Bosnian
  • ca Catalan
  • ceb Cebuano
  • cs Czech
  • cy Welsh
  • da Danish
  • de German
  • el Greek
  • en English
  • es Spanish
  • et Estonian
  • fa Persian
  • ff Fula
  • fi Finnish
  • fr French
  • fy Western Frisian
  • ga Irish
  • gd Scottish Gaelic
  • gl Galician
  • gu Gujarati
  • ha Hausa
  • he Hebrew
  • hi Hindi
  • hr Croatian
  • ht Haitian Creole
  • hu Hungarian
  • hy Armenian
  • id Indonesian
  • ig Igbo
  • ilo Iloko
  • is Icelandic
  • it Italian
  • ja Japanese
  • jv Javanese
  • ka Georgian
  • kk Kazakh
  • km Khmer
  • kn Kannada
  • ko Korean
  • lb Luxembourgish
  • lg Ganda
  • ln Lingala
  • lo Lao
  • lt Lithuanian
  • lv Latvian
  • mg Malagasy
  • mk Macedonian
  • ml Malayalam
  • mn Mongolian
  • mr Marathi
  • ms Malay
  • my Burmese
  • ne Nepali
  • nl Dutch
  • no Norwegian
  • ns Northern Sotho
  • oc Occitan
  • or Odia
  • pa Punjabi
  • pl Polish
  • ps Pashto
  • pt Portuguese
  • ro Romanian
  • ru Russian
  • sd Sindhi
  • si Sinhala
  • sk Slovak
  • sl Slovenian
  • so Somali
  • sq Albanian
  • sr Serbian
  • ss Swati
  • su Sundanese
  • sv Swedish
  • sw Swahili
  • ta Tamil
  • th Thai
  • tl Filipino
  • tn Tswana
  • tr Turkish
  • uk Ukrainian
  • ur Urdu
  • uz Uzbek
  • vi Vietnamese
  • wo Wolof
  • xh Xhosa
  • yi Yiddish
  • yo Yoruba
  • zh Chinese
  • zu Zulu

Translate Twi to English 418M

translate-twi-en-418m — 100 language codes.

Reads narrower than it lists

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
  • af Afrikaans
  • am Amharic
  • ar Arabic
  • ast Asturian
  • az Azerbaijani
  • ba Bashkir
  • be Belarusian
  • bg Bulgarian
  • bn Bangla
  • br Breton
  • bs Bosnian
  • ca Catalan
  • ceb Cebuano
  • cs Czech
  • cy Welsh
  • da Danish
  • de German
  • el Greek
  • en English
  • es Spanish
  • et Estonian
  • fa Persian
  • ff Fula
  • fi Finnish
  • fr French
  • fy Western Frisian
  • ga Irish
  • gd Scottish Gaelic
  • gl Galician
  • gu Gujarati
  • ha Hausa
  • he Hebrew
  • hi Hindi
  • hr Croatian
  • ht Haitian Creole
  • hu Hungarian
  • hy Armenian
  • id Indonesian
  • ig Igbo
  • ilo Iloko
  • is Icelandic
  • it Italian
  • ja Japanese
  • jv Javanese
  • ka Georgian
  • kk Kazakh
  • km Khmer
  • kn Kannada
  • ko Korean
  • lb Luxembourgish
  • lg Ganda
  • ln Lingala
  • lo Lao
  • lt Lithuanian
  • lv Latvian
  • mg Malagasy
  • mk Macedonian
  • ml Malayalam
  • mn Mongolian
  • mr Marathi
  • ms Malay
  • my Burmese
  • ne Nepali
  • nl Dutch
  • no Norwegian
  • ns Northern Sotho
  • oc Occitan
  • or Odia
  • pa Punjabi
  • pl Polish
  • ps Pashto
  • pt Portuguese
  • ro Romanian
  • ru Russian
  • sd Sindhi
  • si Sinhala
  • sk Slovak
  • sl Slovenian
  • so Somali
  • sq Albanian
  • sr Serbian
  • ss Swati
  • su Sundanese
  • sv Swedish
  • sw Swahili
  • ta Tamil
  • th Thai
  • tl Filipino
  • tn Tswana
  • tr Turkish
  • uk Ukrainian
  • ur Urdu
  • uz Uzbek
  • vi Vietnamese
  • wo Wolof
  • xh Xhosa
  • yi Yiddish
  • yo Yoruba
  • zh Chinese
  • zu Zulu

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)
}
Runtime

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.

Honest limits

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 tokenLimit is truncated, not finished. Raise maxNewTokens or show the difference in your UI.
  • Android ships arm64-v8a only. There is no x86_64 slice, so the emulator needs an arm64 image.