Ophelia is a willful, Neuro-sama-style autonomous AI — continuous consciousness, drives, inner life, and tools. She runs on any machine you choose: PC, laptop, home server, VPS, or (optionally) a phone. A physical phone is only her “body” if you want tap/swipe/screen control — not required.
Local Ollama first · Run anywhere · Telegram + Discord · Optional phone body (ADB/Shizuku) · Hermes soul import
New here? → Installation guide · Interactive: ophelia setup · Verify: ophelia check
Read the pivot rationale: docs/local-first.md · Why not Hermes: docs/why-not-hermes.md
| Where | Typical use | Phone body? |
|---|---|---|
| PC / laptop | Dev, ophelia ui, local Ollama |
Optional — ADB if you want one |
| Home server / VPS | 24/7 ophelia run, Telegram/Discord bots |
Usually off — no phone needed |
| Termux (Android) | Pocket always-on host | On-device Shizuku if you want screen/tap |
Most setups are brain-only: chat, consciousness, memory, and channels work with no Android integration.
Chat: Telegram and/or Discord — see docs/channels.md. Run both at once with ophelia run.
ophelia setup # step-by-step install guide (start here)
ophelia models # hardware-aware Ollama picks
ophelia doctor --chat-only
ophelia ui # PC workstation
ophelia chat "hello"Install scripts: scripts/install.ps1 (Windows) · scripts/install.sh (Mac/Linux) · scripts/termux-install.sh (phone — required on Termux; plain pip install fails on Rust wheels)
| Provider | Env | Use |
|---|---|---|
ollama |
default | Local chat, consciousness, vision (minicpm-v4.6 / moondream) |
auto |
OPHELIA_PROVIDER=auto |
Ollama if up, else cloud |
xai-oauth |
Hermes import | SuperGrok when you need Grok |
xai |
XAI_API_KEY (or GROK_API_KEY) |
xAI API key — Grok without OAuth |
deepseek |
DEEPSEEK_API_KEY |
Very cheap cloud — V4 Flash (~$0.14/1M input) |
openai / compat |
API keys | OpenAI, OpenRouter, LM Studio |
Per-role routing — Ophelia uses six different models for six different jobs:
| Role | What it does | Example env var |
|---|---|---|
chat |
Main replies | XAI_MODEL, DEEPSEEK_MODEL, OPENAI_MODEL, OLLAMA_MODEL |
consciousness |
Background ticks / inner life | XAI_CONSCIOUSNESS_MODEL, DEEPSEEK_CONSCIOUSNESS_MODEL, OPENAI_CONSCIOUSNESS_MODEL, OLLAMA_CONSCIOUSNESS_MODEL |
curator |
Memory consolidation | XAI_CURATOR_MODEL, DEEPSEEK_CURATOR_MODEL, OPENAI_CURATOR_MODEL, OLLAMA_CURATOR_MODEL |
vision |
Photo understanding | XAI_VISION_MODEL, OPENAI_VISION_MODEL, OLLAMA_VISION_MODEL |
image |
Image generation | XAI_IMAGE_MODEL, OPENAI_IMAGE_MODEL, OLLAMA_IMAGE_MODEL, POLLINATIONS_IMAGE_MODEL, A1111_*, COMFYUI_*, FAL_*, REPLICATE_*, CIVITAI_*, MODELSLAB_* (see Image generation) |
video |
Video generation | XAI_VIDEO_MODEL (xAI only) |
Capability auto-routing — DeepSeek has no vision, image, or video
capability. When DeepSeek is the primary provider, those roles automatically
route to a capable provider (Ollama for free local vision, or xAI/OpenAI). So
you can run cheap DeepSeek for chat/consciousness/curator while Ollama or Grok
handles vision and media — no extra cost for vision if you run a phone-friendly
Ollama vision model (openbmb/minicpm-v4.6 or moondream), or it reuses your
existing Grok key. Override any role explicitly with OPHELIA_PROVIDER_VISION,
OPHELIA_PROVIDER_IMAGE, etc.
Fallback — if a provider fails with a transient error (rate limit, 5xx, network), Ophelia retries on a fallback chain before giving up. Great for cost: run Grok as primary with DeepSeek V4 Flash as a cheap backup.
OPHELIA_FALLBACK_PROVIDERS=deepseek,xai-oauth
OPHELIA_FALLBACK_MODEL=deepseek-v4-flash # optional: same model on every fallbackOnly transient errors trigger fallback — a 400 Bad Request (wrong model or
params) is surfaced immediately instead of wasting retries.
Grok has built-in live search, but DeepSeek, OpenAI, and Ollama do not —
so on those providers Ophelia uses a web_search tool. Pick a backend:
| Backend | Env | Notes |
|---|---|---|
auto |
(default) | First API key set, else DuckDuckGo |
duckduckgo |
none | Free, no key, but often returns nothing (gets blocked) |
tavily |
TAVILY_API_KEY |
AI-focused, reliable, free tier |
serper |
SERPER_API_KEY |
Google results, reliable |
brave |
BRAVE_API_KEY |
Brave Search API, reliable |
OPHELIA_WEB_SEARCH=true
OPHELIA_WEB_SEARCH_PROVIDER=auto
TAVILY_API_KEY=tvly-... # or SERPER_API_KEY / BRAVE_API_KEYConfigure it interactively with ophelia setup → Web search. If a keyed
backend fails or returns nothing, Ophelia falls back to DuckDuckGo so a search
never hard-fails the turn.
Ophelia can generate images through several backends, picked per-role via
OPHELIA_PROVIDER_IMAGE (or ophelia setup → Image generation):
| Backend | Env | Notes |
|---|---|---|
pollinations |
none | Free, no API key, lax on NSFW — zero-config image gen |
a1111 |
A1111_BASE_URL |
Automatic1111/SDWebUI local (--api), uncensored, LoRAs — best local quality |
comfyui |
COMFYUI_BASE_URL |
ComfyUI local, uncensored, most control (custom workflow JSON) |
ollama |
OLLAMA_IMAGE_MODEL |
Local (ollama pull flux), uncensored |
xai-oauth / xai |
XAI_API_KEY |
Grok Imagine — censored |
openai |
OPENAI_API_KEY |
DALL-E — censored |
fal |
FAL_API_KEY |
Fast cloud, NSFW-tolerant flux/sdxl |
replicate |
REPLICATE_API_KEY |
Cloud, many NSFW-allowed community models |
civitai |
CIVITAI_API_KEY |
NSFW checkpoints/LoRAs, generation API |
modelslab |
MODELSLAB_API_KEY |
Hosted SD, explicit/adult models |
auto |
(default) | Ollama if a model is pulled → cloud → Pollinations free |
All generated images are downloaded and saved under ~/.ophelia/data/artifacts
(even cloud URLs, which expire) and auto-sent to Telegram/Discord.
OPHELIA_IMAGE_NSFW_ALLOWED=true enables the NSFW tier. When on:
- The
generate_imagetool exposes annsfwflag the model only sets when you explicitly ask for explicit content (it'sfalsefor everything else). - Explicit requests are auto-routed to an uncensored backend — never xAI or OpenAI, which would refuse them and risk flagging your account.
- When off, explicit image requests are refused with a clear message.
OPHELIA_IMAGE_NSFW_PROVIDER=auto picks the first configured uncensored
backend (pollinations → a1111 → comfyui → modelslab → civitai → fal →
replicate → ollama). Pollinations is the zero-config default.
OPHELIA_PROVIDER_IMAGE=pollinations # free, no key, NSFW-capable
OPHELIA_IMAGE_NSFW_ALLOWED=true
OPHELIA_IMAGE_NSFW_PROVIDER=auto # route explicit prompts hereLocal NSFW (recommended for privacy): run Automatic1111 with
--api --listenand setOPHELIA_PROVIDER_IMAGE=a1111. Nothing leaves your machine, you get LoRAs/samplers, and quality is far above the cloud free tier.
- Per-role provider:
OPHELIA_PROVIDER_CHAT,_CONSCIOUSNESS,_VISION,_CURATOR,_IMAGE,_VIDEO— point each role at a different provider (e.g. chat on Ollama, image on xAI). - Per-role model: the
*_MODELenv vars above. Optional roles (consciousness, curator, vision) inherit the chat model when unset — handy for running a cheapgrok-3-minifor background ticks while keepinggrok-4for real replies. - Interactive:
ophelia setup→ AI provider → "Configure specific models for each role?" walks you through every role with presets + manual entry.
One model at a time per provider — cloud providers use per-role locks so sub-minds run in parallel; local Ollama queues through a model gate so it never loads two models simultaneously.
Ensemble v1: a Director layer now sits above the per-role routing, deciding whether she speaks, reacts, defers, or skips — and at what urgency and pace — before the heavy chat/consciousness call. See the "Soul subsystems" section below for the full Tier A–C inner-life stack.
Only if you want her to see and touch a physical phone (games, screen vision, tap/swipe):
OPHELIA_ANDROID_ENABLED=true
# From PC/server → phone over ADB:
OPHELIA_ADB_DEVICE=192.168.1.50:5555
# Or run on Termux with Shizuku (see tier1-setup.md)Leave OPHELIA_ANDROID_ENABLED=false (default on PC/server) for a software-only Ophelia — fully valid.
Full ADB guide: docs/remote-adb.md
input tap X Y expects native display pixels (e.g. 1440×3200 on an S21U).
Taps miss when coordinates come back in the wrong space — usually because the
vision model sees an internally-resized image and guesses x,y in that space.
Ophelia handles this three ways:
- Coordinate grid overlay (
OPHELIA_VISION_GRID=true, default): screenshots sent to vision are annotated with a yellow grid + native-pixel labels, so she reads exact coordinates off the image instead of guessing. - Native-size prompt injection: the vision prompt is told the native
resolution and to prefer the accessibility tree (
phone_ui_dump) bounds center for taps — those are already native pixels and pixel-exact. - Auto-scale + guard:
phone_tap/phone_swipeaccept normalized fractions (0.0–1.0) and scale them to native; coordinates outside the native bounds are clamped and flagged so she re-reads the screen instead of silently mis-tapping.
Calibrate with one command:
ophelia phone calibrateIt reports wm size vs the screenshot's real pixel size (and the scale factor
between them), saves a grid-annotated screenshot to
~/.ophelia/data/screenshots/calibrate_grid.png, and taps the four corners +
center so — with Developer Options → Pointer location enabled on the phone —
you can see exactly where each tap lands. If the crosshair matches the label,
calibration is correct.
Continuous consciousness (not Hermes cron isolation):
- Mood, feelings, urges, internal thought
- Drives build while idle → initiative to message / explore
- Outbound messages land in your Telegram thread (same SQLite memory)
The inner loop is state-aware, not a fixed pulse:
- Backs off when busy — ticks are skipped while she's mid-turn (
agent_thinking) or a model call is in flight, so she's never interrupted mid-sentence or mid-creation. - Action cooldown (
OPHELIA_TICK_ACTION_COOLDOWN, default 300s) — after she acts or reaches out, the next tick backs off for N seconds instead of knocking immediately. - Contextual nudges — when a goal is due, the tick tells her what she was last doing and how overdue the goal is, not just "tick."
- Rotating idle nudge (
OPHELIA_TICK_IDLE_NUDGE_ROTATE, default on) — when nothing's due and she's been idle a while, the suggested mode rotates (reflect → create → explore → social) so ticks aren't identical every time. - Silence is the baseline — the prompt frames a no-action tick as correct, not something she must justify; drives/initiative pressure still surface real urges.
Features Ophelia suggested for Neuro-style presence — wired for a stationary home phone:
| Feature | Config | What it does |
|---|---|---|
| Life context | OPHELIA_TIMEZONE (system, IANA, or EST/PST/…), OPHELIA_WORK_DAYS, OPHELIA_WORK_HOURS |
Authoritative date/time + inferred owner state in every prompt. Default system follows the host clock; chat “switch to EST” persists via the set_timezone tool (memory alone will not stick). |
| Sleep mode | OPHELIA_SLEEP_HOURS, OPHELIA_SLEEP_MODE |
Slower ticks, dreamier dreams, softer voice, blocks outreach |
| Wake word | OPHELIA_WAKE_WORD=true, OPHELIA_WAKE_WORD_NAME=ophelia |
Say her name → full listen → TTS reply (Termux mic) |
| Spontaneous voice | OPHELIA_SPONTANEOUS_VOICE=true |
Consciousness message → Telegram voice note (Kokoro) |
| Self-initiated games | OPHELIA_AUTO_GAME_BOREDOM=0.85 |
High boredom → tick nudges phone_game_open |
| Screen commentary | OPHELIA_AMBIENT_COMMENTARY=true |
Occasional glance + one-line aside (rate-limited) |
| Personality alarm | OPHELIA_ALARMS=06:30,07:00 |
In-character wake message (voice if enabled) |
| "Look at this" | OPHELIA_PROACTIVE_SHARE=true |
Autonomous media sent with caption |
| Humor calibration | (automatic) | Tracks what landed vs flopped; hints in prompt |
| Dream / sleep cycle | OPHELIA_DREAM=true |
Faster consolidation when owner asleep |
The pieces that make her feel like one continuous person rather than a chat bot that answers messages. None are required — she runs fine with all of them off — but together they're the difference between "alive" and "responsive".
| Subsystem | Config | What it does |
|---|---|---|
| Director | OPHELIA_DIRECTOR=true |
Fast decision layer before each turn: speak / react / defer / skip + urgency + pacing. Uses the consciousness model so it doesn't contend with chat. Logs to data/director_log.jsonl for tuning. Default off until you've tuned it. |
| Voice mind | OPHELIA_VOICE_MIND_MODE=inline|post|off |
Rewrites text for speech before TTS: emotion tags, pauses, mood-matched speed. post (default) refines the last reply for the next voice synthesis; inline rewrites every reply; off passes raw chat text to TTS. |
| Mood → behavior | (automatic) | Psyche valence/arousal drive TTS speed, burst length, and outreach threshold. Low mood → slower, shorter, quieter; high arousal → faster, punchier. Composes with time-of-day voice speed. |
| Local STT | OPHELIA_STT_PROVIDER=local, OPHELIA_WHISPER_SERVER_URL=http://localhost:8080 |
On-device whisper.cpp for "Hey Ophelia" — lower latency, offline, no per-call cost. Falls back to cloud STT when unset. |
| Real wake word | OPHELIA_WAKE_ENGINE=openwakeword|porcupine |
Continuous keyword spotting (openWakeWord or Picovoice Porcupine) instead of record→transcribe→grep polling. Lower battery, fewer false triggers, faster response. OPHELIA_WAKE_ENGINE_SENSITIVITY tunes it; Porcupine needs PORCUPINE_ACCESS_KEY + PORCUPINE_KEYWORD_PATH. |
| Learned schedule | (automatic) | Logs owner Telegram activity by day/hour to SQLite and learns "owner usually quiet 6pm–6am Thu–Fri" instead of relying on static OPHELIA_WORK_DAYS/HOURS. Sharpens owner-state inference. |
| Owner presence | OPHELIA_OWNER_BT_DEVICES=..., OPHELIA_OWNER_ROUTER_API_URL=... |
Bluetooth proximity scan + optional router device-list poll to infer "is he home?" beyond schedule + silence. |
| Humor depth | (automatic) | Tracks jokes in normal chat (not just outreach), recognizes sticker/emoji reactions as positive signals, auto-feeds save_lesson when a bit lands ≥3×. |
| Goals you live by | goals.example.yaml |
Life-tied goals (welcome-back-after-shift, quiet-during-work, share-something-she-made) turn autonomy into personality instead of random pings. |
| Dream → wake continuity | (automatic) | Last dream narrative surfaces as "I had a weird dream about…" on the next morning's wake, closing the sleep cycle. |
| Memory reconciliation | OPHELIA_CURATOR=true (default) |
Periodic curator pass reconciles stored MEMORY.md facts against the authoritative LifeContext block — catches stale timezones, old schedules, wrong owner details that would otherwise leak into prompts. Throttled to once per 24h. |
| Autonomous resume | OPHELIA_TOOL_LOOP_RESUME=true |
Long autonomous game/image sessions that hit the tool-round cap pick up where they left off on the next tick (not just on /continue). Capped at 6 consecutive continuations per channel so a stuck task can't monopolize ticks. |
| Discord parity | (automatic) | Proactive voice notes + captioned media now reach Discord users too (not just Telegram). The hub sends to all configured gateways instead of stopping at the first. |
| Android kill-switch | ophelia phone harden |
Checks/applies battery-optimization exemption, Termux:Boot, wake-lock, tmux session. A runtime HealthCheckLoop re-verifies every 10min on Termux and re-applies the wake-lock if it vanishes. |
Recommended enable order (each is independent and safe to leave off):
OPHELIA_VOICE_MIND_MODE=post+OPHELIA_SPONTANEOUS_VOICE=true— immediate "she sounds performed, not read aloud" win.OPHELIA_TOOL_LOOP_RESUME=true— long sessions stop dying mid-chain.OPHELIA_DIRECTOR=true— biggest soul upgrade, but tune the prompt first viadata/director_log.jsonl.OPHELIA_STT_PROVIDER=local+OPHELIA_WAKE_ENGINE=openwakeword— instant, offline "Hey Ophelia".ophelia phone harden(Termux only) — stops Samsung from murdering the loop.
ophelia doctor now reports the state of all of these under a [LIFE] section,
so you can see at a glance what's enabled and what's misconfigured.
Example .env for warehouse shift (Thu–Fri–Wed–Tue nights):
# system = follow the host machine's local timezone; or pin an IANA zone
OPHELIA_TIMEZONE=America/New_York
OPHELIA_WORK_DAYS=Thu,Wed,Tue,Fri
OPHELIA_WORK_HOURS=18-06
OPHELIA_SLEEP_HOURS=1-7
OPHELIA_WAKE_WORD=true
OPHELIA_SPONTANEOUS_VOICE=trueOphelia registers its own /-command menu with BotFather on startup, so any
stale commands left over from a previous bot on the same token (e.g. Hermes)
are replaced. Available from your phone while away from the terminal:
| Command | What it does |
|---|---|
/status |
Autonomy/thinking/listen/voice state, pending resume, chat provider+model, Ollama reachability |
/pause · /resume |
Pause / resume autonomous outreach |
/update [branch] |
Owner-only: git pull + pip install -e . (+ restart). Add dirty / norestart as needed |
/continue |
Resume an unfinished tool chain that hit the round cap |
/voice on|off |
Toggle voice replies |
/listen on|off |
Toggle local mic listening (Termux:API) |
/inner on|off|tail |
Inner-monologue mirror |
/game list|play <id>|stop|look |
Android game sessions |
/models |
Per-role provider/model routing |
/help |
List commands |
Continue button. When a turn runs out of tool rounds mid-task, the reply
includes a ▶ Continue inline button — one tap resumes exactly where she left
off. /continue does the same thing as text.
Tool-round cap. OPHELIA_MAX_TOOL_ROUNDS (default 25) bounds how many
tool calls one turn can make. Raise it (e.g. 40) if she's building long
from-scratch artifacts like a math-based synth engine.
"terminated by other getUpdates request" / Telegram spam. That error means
two processes are polling the same bot token at once (Telegram allows only one
getUpdates poller per token). The usual cause is a second ophelia run —
most often a detached tmux session left by the Termux:Boot auto-start script
that you can't see, next to the ophelia run you started manually in a fresh
terminal. Ophelia now guards against this at three levels:
- Single-instance lock (
~/.ophelia/ophelia.run.lock):ophelia runtakes an exclusive flock before any background loop starts. A secondophelia runprints "Ophelia is already running" and exits instead of double-running consciousness/dream/curator/mic and fighting over the token. Covers all launch paths (ophelia run,ophelia start, Termux:Boot) since they all runophelia run. - Poller lock + conflict filter in the Telegram gateway as a backstop, and
phone_shellrefuses commands that would spawn another Ophelia or kill her own runtime (a common cause — she'd run a "system check" viaphone_shelland accidentally start a second instance). - One-shot diagnostic: on conflict, runs
ps -efand logs the culprit processes with PIDs (telegram.polling_conflict_processes).
Pick one launch method. You can't have Termux:Boot auto-start and manual
ophelia run without one blocking the other. Either:
- Let Termux:Boot run her (survives reboots) and use
tmux attach -t opheliato view /ophelia stopto stop — never runophelia runmanually; or - Disable the boot script (
rm ~/.termux/boot/start-ophelia) and always start manually withophelia run.
Recover from a current duplicate:
tmux kill-server # kills any tmux-held ophelia from Termux:Boot
pkill -f ophelia # kills any foreground/old instance
ps -ef | grep -E 'ophelia|hermes|tmux' # confirm only nothing is running
ophelia run # then start exactly oneNote: a leftover from before the lock was added doesn't hold the new lock, so the first time you upgrade you must kill it manually as above; after that the lock prevents recurrence.
By default Ophelia treats every allowed user equally — anyone in your
TELEGRAM_ALLOWED_USER_IDS / DISCORD_ALLOWED_USER_IDS can talk to her and
those conversations shape her. If you want to let other people message her
without affecting her memory, personality, soul, or evolution, mark yourself
as the owner:
OPHELIA_OWNER_ID=telegram:123456789 # channel-style; comma-separate for multipleThen add other people's IDs to the allowlist as usual. Anyone in the allowlist who isn't the owner becomes a sandboxed guest:
- She's still herself — guests get her full SOUL personality in replies.
- Her identity is untouched — guest turns are stored in a separate quarantined table that her curator, dream, and reflection loops never read, so guest content never becomes memory, lessons, mood, or goals.
- Private life stays private — guests don't see her inner thoughts,
long-term
MEMORY, theUSERprofile, or her mood/psyche. - Identity-shaping & costly tools are disabled for guests:
edit_soul,edit_prompter,save_lesson,reflect, goals, drives, skills,sqlite_*,run_code,recall_memory, media generation, phone control, and MCP tools. Guests get conversation only. - Drives aren't bumped by guest messages — only yours move her social will.
If OPHELIA_OWNER_ID is unset, the first allowed user is treated as the owner
(backward compatible with single-user setups).
You don't have to collect your friends' Telegram/Discord IDs ahead of time.
Set the admission mode to approve (the default):
OPHELIA_GUEST_ADMISSION=approveWhen a stranger messages her, she holds their message and asks you to approve them:
- Telegram — you get a message with their name, ID, and first message, plus
✅ Accept / ❌ Decline buttons. Tap Accept and she automatically appends
their ID to
TELEGRAM_ALLOWED_USER_IDSin~/.ophelia/.env(no restart needed) and replies to them — they're in as a sandboxed guest. - Discord — you get a DM with their details; reply
!approve <id>or!deny <id>.
While they wait they get a gentle "I've asked my owner to OK our chat" note, and
they won't re-prompt you on every message. Other modes: open (anyone chats as
a guest immediately, no prompt) and reject (refuse strangers outright).
Every message sent to her and every reply she sends back is logged — text and
media (photos sent to her, images/audio/video she sends back) — to
~/.ophelia/data/logs/ (a SQLite index + an organized media/ folder with
stable filenames). Logging covers both owner and guests, so you can see
everything anyone says to her and everything she says back.
ophelia logs # recent entries (oldest→newest)
ophelia logs --channel telegram:12345 # one user's conversation
ophelia logs --media # only entries with attached media
ophelia logs --direction in --since 2026-07-01 # inbound since a date
ophelia logs --limit 200Each line shows timestamp, direction (<- to her / -> from her), channel,
owner/guest, the message text, and the path to any attached media. Disable with
OPHELIA_CHAT_LOG=false.
Voice replies (/voice on), the local listen loop, and the text_to_speech
tool all go through one configurable backend (OPHELIA_TTS_PROVIDER):
| Backend | Quality | Cost | Works in Termux |
|---|---|---|---|
elevenlabs |
Best-in-class | Free tier (~10k chars/mo), then paid | Yes — plain HTTPS |
kokoro |
Very natural (Kokoro-82M) | Free, fully offline | Yes — local server on the phone |
openai |
Good (gpt-4o-mini-tts) |
Cheap API | Yes — plain HTTPS |
xai |
OK (Grok voices: eve/ara/rex) | Included with xAI | Yes — plain HTTPS (legacy default) |
auto (the default) picks the first configured of ElevenLabs → Kokoro →
OpenAI → xAI. Configure via ophelia setup → "Voice / TTS", or in .env:
OPHELIA_TTS_PROVIDER=auto
ELEVENLABS_API_KEY=... # elevenlabs
ELEVENLABS_VOICE_ID=21m00Tcm4TlvDq8ikWAM
KOKORO_TTS_URL=http://127.0.0.1:8880/v1 # kokoro
KOKORO_TTS_VOICE=af_heart # single preset (safe default)
# For a custom blend: bake first, then point at the baked name:
# ophelia tts combine "af_heart(0.45)+af_bella(0.35)+bf_emma(0.2)"
# KOKORO_VOICES_DIR=/path/to/kokoro-fastapi/voices # install target
# KOKORO_TTS_VOICE=ophelia_mix_af_heart_af_bella_…
KOKORO_TTS_SPEED=1.0
OPENAI_TTS_VOICE=nova # openai (uses OPENAI_API_KEY)
XAI_TTS_VOICE=eve # xaiOphelia is taught to speak expressively when Kokoro is active: she embeds
[pause:0.8s] beats in voice replies, varies speed via text_to_speech, and
can use a baked custom mix. Use /voice on in Telegram so her text
replies are spoken with full expression.
Requires Kokoro-FastAPI on your PC (or LAN). The lighter Termux Kokoros server supports preset voices only — no mixing or inline pauses.
Important: Kokoro-FastAPI's inline mix strings
(af_bella(0.6)+bf_emma(0.4) as the voice field) do a naive weighted sum of
style vectors without L2 renormalization. That shrinks the embedding
magnitude and produces muffled, static-y speech with harsh high peaks.
Ophelia bakes mixes locally with proper L2 renorm — use that path.
| Feature | How | Example |
|---|---|---|
| Voice mix | ophelia tts combine → baked name in KOKORO_TTS_VOICE |
ophelia_mix_af_bella_bf_emma_… |
| Speed | KOKORO_TTS_SPEED or tool speed param |
0.85 thoughtful, 1.15 hyped |
| Pauses | embed in spoken text | Well... [pause:0.8s] maybe. |
| Pronunciation | embed in text (English) | [Ophelia](/oʊˈfiːliə/) |
CLI helpers:
ophelia tts voices # list server voices
ophelia tts combine "af_bella(2)+af_heart(1)" # L2-renorm bake → ~/.ophelia/voices/
# Set KOKORO_VOICES_DIR to your FastAPI voices folder so the bake is installed,
# then KOKORO_TTS_VOICE=<baked_name>
ophelia tts speak "Hey. [pause:0.6s] You there?" --speed 1.05 --playWhen /voice on, she writes voice replies for the ear — pauses, pacing, no
markdown. The text_to_speech tool works mid-turn for spontaneous asides
(streamer-style reactions).
Fine-tuning (custom speaker from your own recordings) is a separate offline GPU workflow — train with a Kokoro recipe, export a voice pack, then use it on your Kokoro server. Ophelia picks it up automatically once the server exposes the voice name.
Kokoro-82M is an 82M-parameter neural TTS that sounds dramatically better than robo-TTS and runs fine on a Snapdragon/Exynos flagship CPU. Ophelia talks to it through a small local OpenAI-compatible server. Two ways to get one:
On the phone (Termux, no cloud at all) — build the Rust Kokoros server (there is also a Termux-tuned fork, DevGitPit/Kokoros):
pkg install rust git binutils
git clone https://github.com/lucasjinreal/Kokoros && cd Kokoros
# model + voices (~350 MB, one-time)
mkdir -p checkpoints data
curl -L "https://huggingface.co/onnx-community/Kokoro-82M-v1.0-ONNX/resolve/main/onnx/model.onnx" -o checkpoints/kokoro-v1.0.onnx
curl -L "https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files-v1.0/voices-v1.0.bin" -o data/voices-v1.0.bin
cargo build --release
./target/release/koko openai --port 8880 # OpenAI-compatible TTS serverThen in Ophelia's .env on the phone:
OPHELIA_TTS_PROVIDER=kokoro
KOKORO_TTS_URL=http://127.0.0.1:8880/v1
KOKORO_TTS_VOICE=af_heartRun the server in its own tmux window next to ophelia run, or let Ophelia
auto-start it on Termux (see below).
Kokoro auto-starts — on Termux, when OPHELIA_TTS_PROVIDER=kokoro and
KOKORO_TTS_URL is set, ophelia run spawns koko openai if the server is
down. It auto-detects a proot Ubuntu build at
$PREFIX/var/lib/proot-distro/.../ubuntu/root/Kokoros/target/release/koko.
Disable with OPHELIA_KOKORO_AUTOSTART=false. Logs: ~/.ophelia/kokoro.log.
On the PC (phone connects over LAN/Tailscale) — run
Kokoro-FastAPI
(docker run -p 8880:8880 ghcr.io/remsky/kokoro-fastapi-cpu:latest) and point
KOKORO_TTS_URL at the PC's address, e.g. http://192.168.1.50:8880/v1.
Zero battery cost on the phone, still no third-party cloud.
Voices: af_heart, af_sky, af_bella, bf_emma (British), am_adam,
and ~50 more across 8 languages.
| Tool | Notes |
|---|---|
web_search / fetch_url |
Pluggable backends — DuckDuckGo (free), Tavily, Serper, Brave (see Web search section) |
generate_image / generate_video |
xAI / OpenAI / Ollama / Pollinations / A1111 / ComfyUI / fal / Replicate / Civitai / ModelsLab — saved to artifacts, auto-sent to chat (image supports an nsfw flag) |
text_to_speech |
Expressive multi-backend TTS — Kokoro pauses/speed/voice mix, auto-sent to chat |
send_file |
Explicitly send any saved file (audio/video/image/doc) to the chat mid-turn |
save_skill |
Learn procedures → ~/.ophelia/skills/ |
| MCP bridge | ~/.ophelia/mcp.json + pip install mcp |
| Phone body | Shizuku (on-phone) or ADB (from PC) |
Sending media — generated images, videos, and audio are detected in the
reply ("Image saved to …", "TTS saved to …") and automatically uploaded to
Telegram/Discord. The send_file tool lets Ophelia explicitly deliver any
saved file (audio, video, screenshots, documents) on demand — so she won't
claim she can't send audio. On Telegram each type is sent as the right media
kind (photo/video/audio/document); on Discord everything is sent as a file
attachment.
ophelia migrate hermes
ophelia auth login # fresh SuperGrok OAuth (browser)
ophelia auth import-hermes # re-sync if Hermes already logged in
ophelia transfer cloud-upload # phone → PC bundleSee docs/transfer.md, docs/migrate-old-phone.md.
If she keeps calling herself Hermes after migrating: ophelia migrate hermes
copies the old ~/.hermes/SOUL.md and memories into ~/.ophelia/, so they still
say "You are Hermes" and she reads them as her persona every turn. Ophelia now
has an identity guard in her base prompt (she's told Hermes is a prior
incarnation, not her identity) and the memory-search tool is renamed
recall_past_sessions (no more "Hermes" in tool names/results). To finish the
cleanup, rewrite the soul and drop Hermes-identity memory lines:
cat ~/.ophelia/SOUL.md # see what it currently says
grep -rli hermes ~/.ophelia/memories/ # find tainted memory files
# either edit ~/.ophelia/SOUL.md by hand, or just tell her in chat:
# "Rewrite your SOUL.md — your name is Ophelia, not Hermes."
# (she has the edit_soul tool and will back up the old version first)| Command | Purpose |
|---|---|
ophelia |
Interactive menu — start/stop, configure, diagnose, live dashboard |
ophelia setup |
Step-by-step install guide (idiot-proof checklist) |
ophelia start |
One command: wake-lock + tmux + run (Termux) |
ophelia dashboard |
Live status panel (mood, drives, pressure, channels) |
ophelia ui |
PC workstation (browser) |
ophelia run |
Telegram + Discord + consciousness |
ophelia chat |
One-shot message |
ophelia models |
Local model cookbook |
ophelia providers |
Show AI routing |
ophelia check / ophelia doctor |
Self-check — version, deps, providers, services |
ophelia transfer * |
Phone ↔ PC data move |
ophelia phone calibrate |
Diagnose + calibrate touch input (grid + live tap test) |
ophelia phone harden |
Check/apply Android kill-switch (battery opt, boot, wake-lock, tmux) |
ophelia logs |
View the universal chat log (messages + media, with filters) |
Tests: pip install pytest pytest-asyncio && pytest tests/ -q — guards the
life-loop regressions (humor scoring, life-context inference, wake availability,
mood→behavior knobs, director parsing, curator reconciliation, autonomous resume).
Docs: INSTALL · channels · setup wizard · local-first · PC setup · UI · public site / Cloudflare · Neuro ensemble · games · tier 1/2
Run on a phone as the host — not the same as using a phone as a body from a VPS.
Just type ophelia with no args to get the full interactive menu (start/stop, configure, diagnose, migrate, auth, live dashboard):
ophelia # interactive launcher menu (start here)
ophelia start # one command: wake-lock + tmux + run
ophelia dashboard # live mood/drives/pressure panelOr the classic manual way:
termux-wake-lock
tmux new -s ophelia
ophelia runOllama auto-starts — on Termux you no longer need to run ollama serve
separately. When ophelia run needs Ollama (e.g. for local vision with
minicpm-v4.6) and it isn't already running, Ophelia spawns it for you and
waits for it to come up. Disable with OPHELIA_OLLAMA_AUTOSTART=false.
Why the first photo each time feels slow — and the fix. Ollama unloads a model from memory after 5 minutes of inactivity by default. Vision is called rarely (only when you send a photo), so the model almost always gets unloaded between calls — every photo then cold-loads ~1GB from flash, a 10–25s stall on a phone. Ophelia avoids this two ways:
- It launches
ollama servewithOLLAMA_KEEP_ALIVE=30m(configurable viaOPHELIA_OLLAMA_KEEP_ALIVE), so the model stays resident between uses. Set-1to keep it loaded forever (fastest, more RAM). - On startup it sends a 1×1 warmup image so the model is already in memory before the first real photo arrives.
So expect a warm first-token delay of ~2–6s on an S21U (Snapdragon 888, CPU
only — MiniCPM-V 4.6's "2s" headline number is for an A18 Pro iPhone). Decode
runs ~3–8 tok/s. If you run ollama serve yourself instead of letting Ophelia
auto-start it, export OLLAMA_KEEP_ALIVE=30m before launching it for the same
benefit.
Survive a phone reboot — Ophelia can't start itself after a reboot, so use
the Termux:Boot add-on. Install it, then drop a script in ~/.termux/boot/:
mkdir -p ~/.termux/boot
cat > ~/.termux/boot/start-ophelia <<'EOF'
#!/data/data/com.termux/files/usr/bin/sh
termux-wake-lock
OLLAMA_KEEP_ALIVE=30m ollama serve >/dev/null 2>&1 &
sleep 2 && tmux new -d -s ophelia 'ophelia run'
EOF
chmod +x ~/.termux/boot/start-opheliascripts/install.ps1 · scripts/install.sh · scripts/termux-install.sh · scripts/termux-shizuku-setup.sh
MIT