Show HN: Amanuensis – a local-first AI persona that won't fabricate facts A developer released Amanuensis, a local-first AI persona system that runs on a personal GPU machine without cloud LLM calls, designed to prevent fabricated information by requiring human approval before any post is published. The pilot persona, AlexaPavlova, posted as a sarcastic senior Berlin developer on Mastodon and Bluesky, with every post reviewed via Telegram before publication. The project is now an archived experiment, with its MIT-licensed code available for others to fork and run their own AI personas. A local-first AI persona that writes under a human's veto. It drafts, you approve, and nothing it can't ground gets published. A local-first pipeline for running an AI persona on Mastodon and Bluesky. The pilot persona, AlexaPavlova , posts as a sarcastic senior Berlin dev with dry takes on tech news and open source. Everything runs on a local GPU machine. No cloud LLM calls. See it live: Mastodon https://hachyderm.io/@alexa pavlova · Bluesky https://bsky.app/profile/alexapavlova.bsky.social — now disclosed as AI, no longer posting. Every post is reviewed on a phone before it publishes — approve, regenerate text or image, or cancel. Status:this was an experiment, not an active product. The code is MIT-licensed and works end-to-end — fork it, learn from it, run your own persona. Issues and PRs may not get a response. The hard part wasn't generating text, it was stopping the model from fabricating technical detail. The short version: factual-only source summaries, deterministic cleanup before any LLM judgment, a regex pre-screen in front of an LLM grounding check, titles-only memory, and a human approving every post over Telegram. Full write-up of the design and what broke along the way: write-up https://dev.to/msalsas/building-an-ai-persona-that-doesnt-lie-the-parts-nobody-bothers-to-build-33ii . pip install -e ". dev " cp .env.example .env LMStudio - Download LMStudio https://lmstudio.ai/ and load any instruction-tuned model tested with Mistral-7B-Instruct and similar - Go to Local Server → start the server on port 1234 - Set LMSTUDIO BASE URL=http://localhost:1234 in .env SwarmUI - Install SwarmUI https://github.com/mcmonkeyprojects/SwarmUI and load the image model + 41ex4 p4v10v4 LoRA - Set SWARMUI BASE URL=http://localhost:7801 in .env - Message @BotFather https://t.me/botfather → /newbot → copy the token into TELEGRAM BOT TOKEN - Message @userinfobot https://t.me/userinfobot → copy your numeric ID into TELEGRAM CHAT ID - Send any message to your new bot so it can message you back python main batch.py --dry-run This fetches real stories and generates posts + images using your local services. Nothing is written to any database and nothing is sent to Telegram. If this prints 8 posts, your local stack is working. Add your social credentials to .env Mastodon and/or Bluesky — both optional, see table below , then open three terminals : Terminal 1 — generate today's posts and send to Telegram for approval python main batch.py Terminal 2 — listen for your Telegram approvals python main telegram listener.py Terminal 3 — publish approved posts at their scheduled time python main dispatcher.py Approve posts in Telegram. The dispatcher picks them up and publishes. Done. For long-running setups, run the three persistent processes under systemd or supervisord so they survive reboots. main batch.py is a one-shot script — run it via cron or manually each day. The persona's images come from a custom LoRA trained on top of Juggernaut XL "Ragnarok" SDXL , generated through SwarmUI. The LoRA is not included in this repo — it only contains trained deltas, not the base model. LoRA: download from Hugging Face — msalsas/alexa-lora https://huggingface.co/msalsas/alexa-lora . Trigger word 41ex4 p4v10v4 , weight 0.3 , generated at 768×1024. Base model: Juggernaut XL "Ragnarok" by RunDiffusion https://civitai.com/models/133005?modelVersionId=1759168 — get it separately, not distributed here.- The LoRA was trained on a fully synthetic dataset images generated with Juggernaut XL ; the character is not based on any real person. To run the alexa profile with images you need both: load Juggernaut XL in SwarmUI and apply this LoRA. See the model card on Hugging Face for the exact prompt format. Adapters HN, Lobsters, BearBlog, AskHN └── Curator dedup by URL + title + subreddit, banned-topic filter └── BatchFactory ├── Brain LMStudio → post text + image prompt └── ImageService SwarmUI → PNG via LoRA └── Scheduler UTC time windows with jitter └── QueueService SQLite └── TelegramNotifier photo + approval keyboard ├── MastodonPublisher on APPROVE └── BlueskyPublisher on APPROVE Reply pipeline runs in parallel : ReplyListener random poll, 30 min – 2 h per post ├── MastodonCommentFetcher / BlueskyCommentFetcher ├── Brain.evaluate relevance → skip or draft reply ├── Brain.generate reply └── TelegramNotifier REPLY APPROVE / REPLY CANCEL ├── MastodonPublisher.publish reply on APPROVE └── BlueskyPublisher.publish reply on APPROVE - Python 3.10+ LMStudio https://lmstudio.ai/ running locally OpenAI-compatible API SwarmUI https://github.com/mcmonkeyprojects/SwarmUI running locally with the 41ex4 p4v10v4 LoRA loaded- A Telegram bot token + chat ID for the approval workflow - Mastodon and/or Bluesky credentials for publishing - An OpenWeatherMap https://openweathermap.org/api API key free tier, for ambient context in prompts pip install -e ". dev " cp .env.example .env Fill in .env with your tokens and service URLs | Variable | Description | |---|---| LMSTUDIO BASE URL | LMStudio API base, e.g. http://localhost:1234 | SWARMUI BASE URL | SwarmUI base, e.g. http://localhost:7801 | TELEGRAM BOT TOKEN | Bot token from @BotFather | TELEGRAM CHAT ID | Your personal chat ID use @userinfobot to find it | ACTIVE PROFILE | Profile slug, default alexa | WEATHER API KEY | OpenWeatherMap key free | ALEXA MASTODON ACCESS TOKEN | Mastodon token for the alexa profile | ALEXA BLUESKY APP PASSWORD | Bluesky app password for the alexa profile | MASTODON ACCESS TOKEN | Global fallback Mastodon token used if no prefixed var found | MASTODON INSTANCE URL | Fallback — prefer setting mastodon instance url in identity.yaml | BLUESKY HANDLE | Fallback — prefer setting bluesky handle in identity.yaml | BLUESKY APP PASSWORD | Global fallback Bluesky app password | main batch.py generates 8 posts + images and sends each to Telegram for approval run via cron .- You APPROVE / REGEN / CANCEL on your phone. main dispatcher.py publishes approved posts at their scheduled time persistent loop . main reply listener.py polls published posts, drafts replies to incoming comments, and sends them back through Telegram for approval persistent loop . The three persistent loops dispatcher , telegram listener , reply listener belong under systemd or supervisord. python main batch.py --dry-run Fetches real stories, generates text and images via local services, prints everything to stdout. No DB writes, no Telegram, no side effects. python main dispatcher.py --dry-run Logs what would be published for each approved post without making any social API calls. | Script | Purpose | |---|---| main batch.py | Run once daily — fetches stories, generates posts, saves to memory + queue, notifies Telegram | main dispatcher.py | Persistent loop — publishes APPROVED posts at their scheduled UTC time | main telegram listener.py | Persistent loop — handles APPROVE / REGEN / CANCEL / REPLY APPROVE / REPLY CANCEL callbacks | main reply listener.py | Persistent loop — polls published posts for new comments, drafts replies, sends for Telegram approval | Each daily batch generates 8 posts by default profiles/alexa/identity.yaml : | Category | Count | Sources | |---|---|---| | TECH | 5 | Hacker News, Lobste.rs, BearBlog | | PERSONAL | 2 | Ask HN discussion threads | | RAW | 1 | Internally generated no source story | mkdir -p profiles/marco/prompts profiles/marco/generated cp profiles/alexa/identity.yaml profiles/marco/ cp profiles/alexa/prompts/ .j2 profiles/marco/prompts/ Edit profiles/marco/identity.yaml and the .j2 templates Run with the new profile ACTIVE PROFILE=marco python main batch.py --dry-run The directory name slug must match ACTIVE PROFILE . It is separate from the name field in identity.yaml "alexa" vs "AlexaPavlova" . config/ schemas.py RawStory, Post, ProfileConfig Pydantic v2 settings.py Pydantic-settings from .env core/ brain.py LMStudio calls, text cleaning, truncation curator.py Dedup by URL + title + subreddit; banned-topic filter factory.py Orchestrates adapters → curator → brain → image scheduler.py UTC-aware time windows with random jitter profile loader.py Loads profiles/{slug}/identity.yaml adapters/ hn adapter.py Hacker News top stories TECH lobsters adapter.py Lobste.rs hottest TECH bearblog adapter.py BearBlog Discover RSS TECH ask hn adapter.py Ask HN discussion posts via Algolia PERSONAL reddit adapter.py Reddit requires OAuth2 credentials; not used by default services/ memory service.py Per-profile SQLite post history + platform IDs queue service.py Approval queue SQLite image gen.py SwarmUI REST client notification.py Telegram sendPhoto/sendMessage, approval keyboards, long-poll comment service.py comments.sqlite: comments, pending replies, poll state social/ mastodon publisher.py Mastodon REST — publish + publish reply bluesky publisher.py AT Protocol XRPC — publish + publish reply mastodon comment fetcher.py Fetches replies via /api/v1/statuses/{id}/context bluesky comment fetcher.py Fetches replies via app.bsky.feed.getPostThread profiles/ alexa/ identity.yaml Persona config: slots, sources, banned topics, image model prompts/ .j2 Jinja2 templates: system prompt + per-mood + per-platform generated/ Output images slot NNN.png memory.sqlite Post history injected as context into each prompt queue.sqlite Approval queue comments.sqlite Comments, pending replies, poll state pytest tests/ -v 312 tests, all mocked — no live services required All HTTP calls LMStudio, SwarmUI, Telegram, HN, Algolia, etc. are mocked with respx .