Skip to content

Repository files navigation

Textlings

AI characters who text you first — fully local, six dependencies, runs on a Raspberry Pi.

A character/companion messaging app that looks and feels like texting: add characters the way you'd add phone contacts, chat 1-on-1 or in groups, from any browser on your network. Your characters have memories, moods, and small ongoing lives of their own — they reach out when they would, not when a cron job says so. Everything stays on your hardware.

A chat with Luna: multiple short text bubbles, an emoji tapback on the user's message, and a 'searched the web' footer

  • Tiny footprint — Python + SQLite + vanilla JS, no build step, no GPU required. Deps: fastapi, uvicorn, httpx, ddgs, python-multipart (+ pywebpush).
  • Bring your own model — any OpenAI-compatible endpoint: Ollama, llama.cpp server, LM Studio, or a cloud API. Built for small local models (1B–4B) — tool use works through prompt markers, not JSON function calls, because tiny models can actually do that reliably.
  • Private by architecture — chats, memories, and your profile never leave your machine. There is no cloud, no account, no telemetry.
  • Characters are just folderscharacters/<id>/soul.md is the personality, in plain markdown. Edit it in any text editor. Zip it. Share it.
  • Your SillyTavern cards work here — import any V2/V3 PNG character card (chub.ai, etc.) with the ⇪ button; embedded lorebooks come along for the ride. Export back to a dual-spec card that works in every client.

What makes it feel alive

  • They text like people. Replies arrive as several short bubbles with typing pauses, not one paragraph. Characters react to your messages with emoji tapbacks. DMs show delivered/read receipts.
  • They text first. Each chat learns its own rhythm; a character reaches out when the silence stretches past what's normal for you two. Night owls text at 3am. Needy characters double-text. Mention a job interview on Thursday and they'll check in Thursday evening — they schedule it themselves.
  • They have lives. Every character runs small ongoing storylines (mostly ordinary: errands, a friend's birthday, a project) written in their own voice. Ask "what are you up to?" any day and the answer is consistent — and their first-texts reference what's actually going on.
  • They have moods. A rough conversation leaves a mark for a day; a good one does too. Mood colors their tone and nudges how soon they reach out.
  • They know each other. Characters occasionally share offstage moments — a coffee, a call — that land in both their memories. Kai might mention he ran into Luna yesterday. Nobody scripted it.
  • They remember. A shared profile about you, private per-character memories that span DMs and group chats, rolling conversation summaries so long relationships survive a 4k context, and full-history recall with dates. All of it plain markdown you can open, edit, or delete.
  • They can look things up (keyless DuckDuckGo by default) instead of making things up — and send you real photos from the web when it fits the moment.

Quick start

git clone https://github.com/CZ140/textlings.git && cd textlings
python -m venv .venv
.venv/bin/pip install -r requirements.txt        # Linux / Pi / macOS
# .venv\Scripts\pip install -r requirements.txt  # Windows

.venv/bin/python -m app.main

Open http://localhost:8080 — on your phone, use your computer's LAN IP instead.

By default it talks to Ollama at http://localhost:11434/v1. Change the endpoint, model, and everything else from the ⚙ settings screen in the app (or copy config.example.json to config.json and edit it — the app runs fine without one, using built-in defaults). config.json is gitignored because the settings screen stores your LLM API key in it.

Recommended small models

Any chat model on an OpenAI-compatible server works. Good Pi-class options:

Model Size Notes
ministral-3:3b 3B recommended default — stayed concise + in-voice across registers and held distinct group voices in testing (where qwen3:4b caricatured); lightest alignment of the majors, has vision
qwen3:4b-instruct 4B reliable on tool markers, but caricatures richer souls — walls of text, repetition, group voice-collapse
hermes3:3b 3B Nous Research — "system prompt is sovereign" steerability; whatever the soul says, it plays
gemma4:e4b 4.5B eff. great writer but most safety-restricted, and crashes some Ollama versions (ollama#16506)
llama3.2:3b 3B fast, solid character voice
qwen3:1.7b 1.7B for Pi 4 / low RAM

Characters can each run a different model (model in character.json / the editor) — keep everyday characters on the default and give specific characters a more expressive or capable one. scripts/model_probe.py <model...> tests any model for tool-marker obedience and staying-in-character under pressure.

Avoid thinking model variants (plain qwen3:4b, deepseek-r1, etc.) — they burn the whole reply budget reasoning and produce empty texts. Use -instruct tags.

Adding a character

In the app: Contacts → +, fill in name, tagline, greeting, and their soul (personality, voice, boundaries — plain markdown), add a picture. Done.

Or let the AI do it: Contacts → ✨ opens the builder. Either answer a short interview about who you want them to be, or type "model them after Sherlock Holmes" and it researches the subject on the web first. The draft opens in the normal editor for you to review and tweak — nothing is saved until you say so. Set "builder": {"model": "..."} in config.json to use a bigger model just for character creation (drafting a great soul is the hardest job in the app).

Or by hand — create a folder under characters/:

characters/mira/
  soul.md          # personality, voice, example texts — plain markdown
  character.json   # {"name": "Mira", "tagline": "...", "greeting": "..."}
  avatar.png       # optional
  memories.md      # appears over time — what Mira has learned about you
  story.md         # appears over time — what's going on in her life
  mood.md          # appears over time — how she's feeling today

Share a character with the Export .zip button (full fidelity: memories, story, mood) or Export card (SillyTavern-compatible PNG that works in any client). Import either format — zips or community PNG cards — with the ⇪ button in the Contacts header. (Note: the bundled samples Luna and Kai are tracked in git; any characters you create are gitignored by default — they're yours.)

Got a bad reply? Long-press (or right-click) any bubble to regenerate, edit, or delete it.

How memory works

Layer Where What
Shared profile data/user.md Facts every character knows about you. Edit it in ⚙ settings ("About you").
Private memories characters/<id>/memories.md What that character has learned — persists across DMs and group chats.
Chat summary SQLite A rolling summary of older messages, so long chats keep their middle on a 4k context.
Full recall SQLite full-text index Characters can [recall: ...] anything ever said, with dates.

Characters save memories themselves mid-chat (you'll see "✨ Luna will remember that"), and a background pass reviews idle conversations and files away anything durable — including how the conversation left them feeling (mood.md). Everything is plain markdown you can edit or delete.

Characters who text first

Characters can reach out on their own — but on their terms, set per character in the contact editor:

  • Texts first (initiative) — never / rarely / sometimes / often. Each chat learns its own rhythm (how often you text) and a character only reaches out once the silence stretches well past normal — hours for a daily chat, days for an occasional one.
  • Awake hours9-23 for a day person, 20-4 for a night owl who sends "you up?" at 3am. Wraps midnight.
  • Double texts — patient characters send one text and wait; needy ones double- or triple-text with growing gaps, and their last one can acknowledge the silence ("ok it's been 6 hours, i see how it is").

They also schedule their own check-ins from conversation: mention an interview on Thursday and they'll text Thursday evening to ask how it went. Characters are time-aware everywhere — real gaps between messages are visible to them, so they can notice you've been gone two days (when it fits their personality).

Global guardrails live in ⚙ Settings → Behavior (or config.json under proactive): rolling daily cap across all characters, minimum silence floor, and a master on/off switch. Once a character hits their unanswered-text limit they go fully quiet until you reply.

Their own lives

Every character lives through small ongoing stories — usually ordinary stuff (errands, a friend's birthday, fixing something) written in their voice, in characters/<id>/story.md. Beats advance over hours/days, finished stories get archived, and characters spend honest idle stretches "between things."

They also have moods (mood.md — one line, expires after a day) and occasionally share offstage moments with each other: once in a while two characters grab a coffee or trade a call, and the moment lands in both their memory files from each one's perspective. The cast is a small social world, not a row of isolated chatbots.

Photos

  • Send them photos — 📷 in the composer. Vision-capable models (most Ollama multimodal models) actually see the image; text-only models honestly say they can't instead of guessing.
  • They send photos back — characters can share real photos from the web ("the waves today 🌊" + an actual beach shot) when it fits the moment or their story. No selfies — they know they can't take pictures of themselves.

Phone access & notifications (Tailscale)

Characters' first-texts — and replies that land while the app is closed — can show up as real push notifications, and you can use the app from your phone even when you're away from home. Both need an HTTPS origin (browsers only allow the service worker push depends on over HTTPS or localhost; plain http://<host>:8080 won't do).

Tailscale solves both at once, for free, without exposing anything to the public internet — it builds a private network between your own devices and serves the app over real HTTPS. That fits the privacy premise: no cloud relay, the traffic only ever goes between your machines.

One-time setup:

  1. Install Tailscale on the host running Textlings and on your phone, signed into the same account (Pi/Linux: curl -fsSL https://tailscale.com/install.sh | sh then sudo tailscale up; Windows/macOS/phone: https://tailscale.com/download).
  2. Enable HTTPS for your tailnet once in the admin console: turn on MagicDNS and HTTPS Certificates.
  3. Serve the app from the host (Textlings already running on its port):
    tailscale serve --bg 8080
    This prints an https://<host>.<tailnet>.ts.net/ URL that proxies to your local app and works from any device on your tailnet, including your phone on cellular. (Settings → Notifications detects an insecure connection and hands you this exact command for your port.)
  4. Install the PWA: open that …ts.net URL on your phone and add it to the home screen (iOS Safari: Share → Add to Home Screen, then open it from the icon — required for iOS notifications; Android Chrome: menu → Install app).
  5. Enable notifications: ⚙ Settings → Enable notifications on this device and accept the prompt. The toggle is per-device — enable it once on each.

Use Tailscale Serve (above), not Funnel — Serve keeps the app private to your own devices; Funnel would expose this no-auth app to the whole internet. For desktop testing, plain http://localhost:8080 is already a secure context, so notifications work there too.

Tap a notification and it deep-links straight to that chat. If nothing arrives, confirm the host stays awake — push is delivered by the server process, so a sleeping machine sends nothing.

Raspberry Pi deployment

# on the Pi (64-bit OS recommended)
sudo apt install python3-venv
git clone https://github.com/CZ140/textlings.git /home/pi/textlings && cd /home/pi/textlings
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt

# install Ollama and a small model
curl -fsSL https://ollama.com/install.sh | sh
ollama pull ministral-3:3b   # default; qwen3:1.7b for a Pi 4 / low RAM

# set the model in config.json, then:
sudo cp deploy/textlings.service /etc/systemd/system/
sudo systemctl enable --now textlings

Now any phone/laptop on your network can chat at http://<pi-ip>:8080.

Tips for the Pi:

  • Run python scripts/doctor.py first — it checks the AI engine is reachable, the model is installed, and the context window is set high enough (a common silent-truncation trap), in plain language. The same checks back the in-app health probe (GET /api/health/full).
  • Keep context_tokens at 4096 and max_output_tokens ≤ 512.
  • The background memory extractor waits for chats to go idle, so it never competes with live replies. Turn it off in ⚙ Settings → Behavior (or "memory": {"extract_enabled": false}) for zero extra inference.

Security

This app has no real authentication. It binds to 0.0.0.0, so anyone on your network can read your chats and edit your characters.

  • Keep it on a trusted home LAN, or access it remotely through Tailscale/WireGuard instead of port-forwarding.
  • As a courtesy lock (not real auth), set "server": {"auth_token": "something-long"} in config.json and open the app once as http://<host>:8080/?token=something-long.

This is a single-user app for adults, by design. It has no content filter beyond what your chosen model brings — what your characters are like is entirely up to the souls you write.

Project layout

app/            FastAPI backend
  engine.py     reply loop: streaming, tool markers, multi-bubble delivery
  director.py   group-chat speaker selection + reply budget
  memory.py     user.md, memories.md, [remember:], extraction, summaries
  scheduler.py  proactive texting: followups + rhythm pings
  story.py      character storylines     mood.py      daily mood
  offstage.py   characters' offstage moments with each other
  context.py    token budgeting for small context windows
  tools/        web search providers (ddgs, SearXNG)
web/            vanilla JS messaging UI (no build step)
characters/     your characters — plain folders (samples tracked, yours ignored)
data/           SQLite db + user.md (created at runtime, never in git)
scripts/        smoke tests + offline check scripts (no server needed)
tests/          pytest suite (runs in CI; no server or model needed)
deploy/         packaging pipelines: Windows installer + Pi appliance image
PACK_FORMAT.md  character pack spec — how to build shareable packs

License

AGPL-3.0. Run it, change it, share it — if you offer a modified version as a network service, share your changes too.

About

AI character companion texting app - local-first, runs on a Raspberry Pi. Characters with memories, moods, and lives who text you first.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages