<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Raymond Riter</title>
        <link>https://raymondriter.dev</link>
        <description>Long-form posts from Raymond Riter — local-first AI, side projects, smoker recipes, and the occasional rant.</description>
        <lastBuildDate>Tue, 02 Jun 2026 23:45:14 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Raymond Riter</title>
            <url>https://raymondriter.dev/favicon.ico</url>
            <link>https://raymondriter.dev</link>
        </image>
        <copyright>All rights reserved 2026, Raymond Riter</copyright>
        <item>
            <title><![CDATA[GBC-AI: a sermon RAG for my church, running entirely on my own hardware]]></title>
            <link>https://raymondriter.dev/blog/gbc-ai-sermon-rag</link>
            <guid>https://raymondriter.dev/blog/gbc-ai-sermon-rag</guid>
            <pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I attend Grace Bible Church. Every Sunday there's a sermon, posted to YouTube on Monday, and an archive going back years that nobody actually queries. The information is <em>there</em> — it's just locked inside ~30-minute videos with no transcripts, no timestamps, and no way to ask "what did Pastor Bryan say about Romans 8 the last time he preached on it?"</p>
<p>GBC-AI is the answer. It's a local-first sermon RAG that ingests every video, transcribes and diarizes it, indexes the chunks, and lets a congregant ask questions with cited answers that link back to the exact timestamp in the source video.</p>
<h2>The shape of it</h2>
<p>Three entry points, one pipeline behind them:</p>
<ul>
<li><code>app.py</code> — Streamlit chat UI, the thing congregants actually use</li>
<li><code>api/main.py</code> — FastAPI REST surface, the thing future integrations consume</li>
<li><code>launch.py</code> — unified launcher with venv setup, preflight checks, model warm-up</li>
</ul>
<p>The data flow:</p>
<pre class="language-text"><code class="language-text">YouTube / local video
        │
        ▼  yt-dlp
download_manager.py
        │
        ▼  5-tier pipeline
ingest.py
   ├─ faster-whisper        (large-v3-turbo, GPU)
   ├─ pyannote.audio        (speaker diarization)
   ├─ chunker               (semantic + windowed)
   ├─ BGE embeddings        (1024-dim)
   └─ ChromaDB              (persistent vector store)
        │
        ▼
chat_engine.py
   ├─ RAG retrieval         (top-k + hybrid search)
   └─ LM Studio streaming   (currently Gemma-4-26b-a4b)
</code></pre>
<p>A central <code>llm_client.py</code> wraps the streaming client with retries and timeouts. SQLite via <code>database.py</code> replaced an earlier JSON store once the dataset grew. Pydantic env config in <code>settings.py</code> keeps all the secrets and tunables in one place.</p>
<h2>Ten phases, all done</h2>
<p>The implementation plan ran across ten phases — every one is complete and merged:</p>
<ol>
<li><strong>Structured logging</strong> — Loguru everywhere, JSON sinks for prod</li>
<li><strong>Ruff</strong> — locked down style + lint at CI time</li>
<li><strong>Exception hierarchy</strong> — <code>IngestError</code>, <code>RetrievalError</code>, etc., with structured fields</li>
<li><strong>LLM client centralization</strong> — retries, timeouts, token tracking</li>
<li><strong>DB migration</strong> — JSON → SQLite for query-shaped data, ChromaDB stays for vectors</li>
<li><strong>API layer</strong> — FastAPI with versioned routes</li>
<li><strong>Auth</strong> — token-based, integrates with our existing church accounts</li>
<li><strong>Docker</strong> — Compose stack for the API + DB; Streamlit still runs natively</li>
<li><strong>Observability</strong> — Langfuse for trace inspection</li>
<li><strong>Pipeline orchestration</strong> — Prefect for the ingest jobs</li>
</ol>
<p>Last meaningful work was the multi-model routing layer and a Redis-backed task queue. There's an <code>IMPROVEMENT_PLAN_V2.md</code> with the next round of work.</p>
<h2>What runs where</h2>
<p>This whole thing is local-first by design. The 179 GB on disk breaks down roughly as:</p>
<ul>
<li><code>sermon-rag/videos/</code> — raw MP4/MKV, the actual sermons (&gt;100 GB)</li>
<li><code>sermon-rag/audio/</code> — extracted audio + voice separation models</li>
<li><code>sermon-rag/db/</code> — ChromaDB persistent vector store + SQLite</li>
<li>venv with CUDA torch — ~20–40 GB</li>
</ul>
<p>The LLM weights live in LM Studio's own directory, not in the repo. The 24 GB on the RTX 3090 is enough to run a Gemma-4-26b-a4b for the retrieval-augmented Q&amp;A while keeping ChromaDB warm.</p>
<h2>The hard parts</h2>
<p><strong>Whisper accuracy on theological vocabulary.</strong> "Propitiation" and "soteriology" don't appear in Whisper's training data the way "weather" does. I tried hot-word lists and they hurt more than they helped — Whisper's a sequence model, not a dictionary. The fix was a post-process pass that uses an LLM with a custom Bible-specific vocabulary prompt to rewrite obvious mishearings.</p>
<p><strong>Citations with timestamps.</strong> The answer "Pastor Bryan talked about the imputed righteousness of Christ in his Romans 4 series" is useless without a deeplink. Every chunk in ChromaDB carries <code>(video_id, start_seconds, end_seconds)</code>. The chat engine surfaces those as YouTube <code>?t=</code> links in its output, so the user can jump to the moment.</p>
<p><strong>Cache race conditions.</strong> Documented in <code>cache.py:43–46</code> and <code>IMPROVEMENT_PLAN_V2.md</code>. Multiple tabs hitting the API simultaneously could trigger duplicate work. The fix is a proper double-checked lock with a TTL guard, but I'd rather rip the cache out and front the LLM with Redis instead.</p>
<h2>What's next</h2>
<p>The roadmap I have written down:</p>
<ul>
<li>Fix the 26 failing tests in <code>test_ingest.py</code> (heavy module-level imports block clean mocking)</li>
<li>Replace the homegrown cache with Redis-fronted memoization</li>
<li>Add the email-digest feature — weekly auto-summary mailed to opted-in congregants</li>
<li>Sermon comparison — "show me every time this pastor preached on grace, sorted by year"</li>
</ul>
<p>This was the project that taught me how much you can do with a single 24 GB GPU if you're disciplined about model sizes. It's also the project that's most directly useful to people in my actual life — which is its own kind of vindication.</p>
<p>The code is at <a href="https://github.com/Raymondriter/GBC-AI">github.com/Raymondriter/GBC-AI</a>.</p>]]></content:encoded>
            <author>rpr2998@gmail.com (Raymond Riter)</author>
        </item>
        <item>
            <title><![CDATA[helpmetopray.org: a quiet place to pray, and to be prayed for]]></title>
            <link>https://raymondriter.dev/blog/helpmetopray</link>
            <guid>https://raymondriter.dev/blog/helpmetopray</guid>
            <pubDate>Sun, 15 Mar 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p><em>"Where two or three gather in my name, there am I with them."</em> — Matthew 18:20</p>
</blockquote>
<p>Most prayer apps are dressed-up journals. You open them, type a request into a private feed, and that's it. There's no presence on the other side. That's not what prayer is — it's not even what the model in Matthew 18 describes.</p>
<p>So I built <a href="https://helpmetopray.org">helpmetopray.org</a>. The differentiator is not another journal or devotional feed — it's <strong>felt presence</strong>. When you post a request, real people pray for it. When you open the app, you can sit with a stranger's burden for two minutes. Jesus-centered by default, extensible later.</p>
<h2>The four ways in</h2>
<p>Once you're past the landing page, the daily dashboard offers four routes:</p>
<ul>
<li><strong><code>/pray/solo</code></strong> — phrase-by-phrase walk through the Lord's Prayer. The original blueprint Jesus gave when his disciples asked how to pray. Walked slowly, with each line as its own screen.</li>
<li><strong><code>/pray/stranger</code></strong> — you receive one real request submitted by another user. You sit with it. You Amen. The request goes back into the queue for someone else to pray. You don't see the requester's name; they don't see yours.</li>
<li><strong><code>/pray/request</code></strong> — you share a request, with a visibility scope: <strong>private</strong> (just you), <strong>circle</strong> (specific people), or <strong>world</strong> (anonymous, goes into the stranger pool).</li>
<li><strong><code>/pray/journal</code></strong> — your open and answered prayers. The only spot where the experience is journal-shaped.</li>
</ul>
<p>There's also a <code>/pray/alongside</code> placeholder for what I think will be the killer feature: a silent presence room. Open the app, see how many other people are praying right now, sit in the same digital silence. No content, no chat, just presence. That's the v2.</p>
<h2>The anonymous-first decision</h2>
<p>A surprising number of people who want to pray won't sign up for an account to do it. So the entire app works without one.</p>
<p>Anonymous visitors keep everything in their browser's <code>localStorage</code>. They can use <code>/pray/solo</code>, walk the Lord's Prayer, write private requests, and journal answered prayers — all without an account. Sign-in is only required to <em>post a request strangers can pray for</em> or to <em>receive other strangers' requests</em>, because those two flows require a server-side queue.</p>
<p>That asymmetry is the core UX bet. Most apps lock everything behind sign-up. helpmetopray locks only the social actions. The reflective ones are free.</p>
<h2>The stack</h2>
<pre class="language-text"><code class="language-text">Framework        Next.js 16 (App Router) + React 19
Styling          Tailwind v4
State            Zustand with localStorage persistence (anonymous users)
Backend          Supabase (Postgres + Auth + Realtime + Edge Functions)
                 — swapped behind a Backend interface so anonymous mode
                   uses a localStorage adapter, signed-in mode uses Supabase
Deploy           Vercel, custom domain helpmetopray.org
</code></pre>
<p>The <code>Backend</code> interface in <code>src/lib/backend.ts</code> is the thing I'm proudest of architecturally. It lets the entire app stay agnostic about whether you're signed in. Want to test the signed-in flow without a Supabase project? Plug in the mock backend. Want to swap Supabase for something else later? It's one file.</p>
<h2>Why this exists</h2>
<p>I'm a Christian, and I noticed that I would intend to pray and not actually do it. The friction wasn't theological — it was UX. By the time I'd opened my Bible app, scrolled past the daily devotional, dismissed the upsell, and found the prayer journal feature buried three menus deep, the moment had passed.</p>
<p>I wanted something that respected the moment. Open, tap one of four cards, do the thing, close.</p>
<p>The other half of why this exists is the stranger pool. The number of times I've gotten a text from a friend saying "please pray for X" and felt the weight of it lift slightly is significant. That's something software can carry at scale. People are willing to do this for strangers; they just need a place where it's normal to.</p>
<h2>What's next</h2>
<p>The silent presence room is the big one. After that:</p>
<ul>
<li>Push notifications for circle requests (opt-in, never aggressive)</li>
<li>A "your request was prayed for" trickle that returns to the requester anonymously — closes the loop without identifying the prayer</li>
<li>An offline mode that queues requests for sync</li>
</ul>
<p>The site is live now. If you want to use it, just open it. No app to install, no account required to start.</p>
<p><a href="https://helpmetopray.org">helpmetopray.org</a></p>]]></content:encoded>
            <author>rpr2998@gmail.com (Raymond Riter)</author>
        </item>
        <item>
            <title><![CDATA[Job-Shorts: rendering every chapter of the Book of Job as a 60-second AI video]]></title>
            <link>https://raymondriter.dev/blog/job-shorts</link>
            <guid>https://raymondriter.dev/blog/job-shorts</guid>
            <pubDate>Wed, 22 Apr 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>The Bible is public domain. AI video gen is here. Short-form Bible content has a real audience. I wanted to know if I could turn reading Job into a 42-episode YouTube/TikTok series rendered entirely on my desktop.</p>
<p>I'm calling it Job-Shorts. The pipeline is local-first, commercial-safe, and routes every text-generation call through my Claude Code subscription so the per-chapter cost is the electricity it takes the 3090 to render.</p>
<h2>The pipeline</h2>
<pre class="language-text"><code class="language-text">chapter_number + KJV text
        │
        ▼
1. SCRIPT GEN     (Claude via subscription, ~10s)
        → 150-word narration (hook / setup / tension / payoff / turn / CTA)
2. BREAKDOWN      (Claude, ~15s)
        → 4–6 visual beats + character lock + style lock
3. KEYFRAMES      (ComfyUI / Flux Dev / Z-Image)
        → 1 preview image per beat
4. NARRATION      (F5-TTS, local)
        → audio + word-level timestamps
5. VIDEO GEN      (ComfyUI / LTX 2.3 or HunyuanVideo)
        → each beat at narration-matched length, 2 takes
6. EVALUATOR      (Claude vision)
        → picks the best take by scoring rendered frames
7. CAPTIONS       (faster-whisper)
        → burned word-by-word captions from narration
8. ASSEMBLE       (FFmpeg)
        → concat + narration + music bed (sidechain-ducked) + verse overlays
9. PUBLISHING     (Claude)
        → title + description + hashtags + thumbnail
        │
        ▼
output/chapter_N/final.mp4 (1080x1920, vertical, ready to upload)
</code></pre>
<p>End-to-end on the 3090: roughly 30–60 minutes per chapter, mostly unattended.</p>
<h2>The LLM routing trick</h2>
<p>There's a <code>job_shorts.llm</code> module that fronts every text call. It picks one of three backends:</p>
<table><thead><tr><th>Backend</th><th>When picked</th><th>Cost</th></tr></thead><tbody><tr><td><code>claude_code</code></td><td>default if <code>claude</code> is on PATH</td><td>uses your Claude Code subscription</td></tr><tr><td><code>claude_api</code></td><td>only if <code>llm_backend=claude_api</code></td><td>pay-per-token</td></tr><tr><td><code>ollama</code></td><td>fallback</td><td>free, local</td></tr></tbody></table>
<p><code>claude_code</code> works by invoking <code>claude -p</code> as a subprocess and reading from your existing auth. So all script generation, scene breakdown, evaluator scoring, and publishing metadata go through my Max plan — no per-token spend.</p>
<p>That single decision turns the math on its head. Without it, generating 42 chapters at GPT-4-class quality would cost real money. With it, the only cost is the electricity for ComfyUI to render the video.</p>
<h2>Patterns I borrowed (and the ones I had to invent)</h2>
<p>Borrowed from other text-to-film projects — these are well-established now:</p>
<ol>
<li><strong>Character lock</strong> — full physical description injected verbatim into every prompt</li>
<li><strong>Style lock</strong> — 20–40 word visual style string locked across all prompts</li>
<li><strong>World reconstruction</strong> — every prompt fully self-contained, no inter-clip memory assumed</li>
<li><strong>Storyboard-before-video</strong> — cheap keyframe preview before expensive video gen</li>
<li><strong>Multiple takes + AI evaluator</strong> — generate 2–3 takes, LLM scores PASS/FAIL</li>
<li><strong>Duration calc from word count</strong> — narration WPM dictates clip length</li>
<li><strong>Resume-from-crash state file</strong> — JSON state after every step</li>
</ol>
<p>What I had to add for Bible content specifically:</p>
<ol>
<li><strong>Whisper caption timing</strong> — accurate word-level burned captions for muted viewing (this is non-negotiable on Shorts/TikTok)</li>
<li><strong>Series-wide consistency</strong> — <code>series.json</code> keeps Job + style identical across all 42 episodes</li>
<li><strong>KJV auto-fetch</strong> — pulls public-domain Bible text from bible-api.com so I never type a verse</li>
<li><strong>Verse chyron overlay</strong> — quoted scripture appears on screen with proper formatting</li>
<li><strong>Music bed with sidechain ducking</strong> — auto-select + duck under narration</li>
<li><strong>Batch mode</strong> — process N chapters overnight unattended</li>
</ol>
<h2>The CLI surface</h2>
<pre class="language-bash"><code class="language-bash"><span class="token comment"># See your hardware tier and recommended models</span>
python -m job_shorts.cli info

<span class="token comment"># Verify the LLM backend works</span>
python -m job_shorts.cli test-llm

<span class="token comment"># Just write the script — fast, free, no rendering</span>
python -m job_shorts.cli script <span class="token number">1</span>

<span class="token comment"># Generate one chapter end-to-end (supervised)</span>
python -m job_shorts.cli chapter <span class="token number">1</span>

<span class="token comment"># Batch chapters 1 through 10</span>
python -m job_shorts.cli batch <span class="token number">1</span>-10

<span class="token comment"># Fully autonomous overnight: auto-launch services, vision-evaluator, no gates</span>
python -m job_shorts.cli auto-batch <span class="token number">1</span>-42

<span class="token comment"># Resume a crashed run</span>
python -m job_shorts.cli resume output/chapter_03
</code></pre>
<p>The fully-autonomous mode is the one I actually use. Start it, walk away, the rig knows how to relaunch ComfyUI or Ollama if either dies, and there's a JSON state file after every step so a power blip doesn't cost a chapter.</p>
<h2>Where it's at</h2>
<p>Phase 0 (23 modules) is complete. Phase 1 — first end-to-end render — is the next step. The interesting open questions are around the evaluator: how do you teach a vision model to spot when LTX has slipped into uncanny-valley territory before you commit to a take? The current heuristic is naive (luminance variance + character consistency check). I think there's a smarter version that compares each frame back to the keyframe storyboard.</p>
<p>If you want to read the actual code or watch progress, it lives on my GitHub. The whole point of this project is that anyone with a 12 GB+ GPU should be able to fork it and render their own Bible (or any other public-domain text). The pipeline doesn't care that it's Job.</p>]]></content:encoded>
            <author>rpr2998@gmail.com (Raymond Riter)</author>
        </item>
        <item>
            <title><![CDATA[LexiGrow: a clinical-grade tracker for my kids' first words]]></title>
            <link>https://raymondriter.dev/blog/lexigrow</link>
            <guid>https://raymondriter.dev/blog/lexigrow</guid>
            <pubDate>Thu, 26 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>When you have a baby, you start writing down the words. "Dada" got logged on a sticky note. "More" went into the Notes app. By the time you have a toddler you have words in four places, none of them queryable, none of them comparable to anything clinical.</p>
<p>LexiGrow is the app I built so my wife and I would stop losing track. Then I realized the proper version of this tool already existed in the speech-language pathology world — it's just not consumer-facing. So I rebuilt it from the spec.</p>
<h2>MB-CDI as the foundation</h2>
<p>The MacArthur-Bates Communicative Development Inventories are the standard parent-report instrument for early language acquisition. Two relevant forms:</p>
<ul>
<li><strong>Words &amp; Gestures</strong> — 8–18 months, 396 vocabulary items + a gestures inventory</li>
<li><strong>Words &amp; Sentences</strong> — 16–30 months, expanded vocabulary + early grammar</li>
</ul>
<p>LexiGrow ships with both vocabulary banks bundled as JSON (<code>assets/data/mbcdi_vocabulary.json</code>, <code>assets/data/mbcdi_gestures.json</code>). You don't track free-text words — you check off MB-CDI items as your child produces them. That means the data is <strong>directly comparable to clinical norms</strong>, not just a personal log.</p>
<h2>The stack</h2>
<pre class="language-text"><code class="language-text">Framework        Flutter 3.27+ / Dart 3.6+
Local DB         Isar (offline-first, fast, type-safe)
State / arch     BLoC, feature-first
Theming          Material 3 light + dark
Future backend   Firestore for sync (Phase 8 of the modernization plan)
Compliance       COPPA/GDPR services in core/compliance/
</code></pre>
<p>The project layout is feature-first so each module owns its own state:</p>
<pre class="language-text"><code class="language-text">lib/
├── main.dart
├── core/
│   ├── compliance/      COPPA/GDPR services
│   └── theme/           Material 3 light + dark
├── data/
│   ├── models/          Isar collections (VocabularyItem,
│   │                                       GestureItem,
│   │                                       ChildProfile)
│   └── repositories/    Isar-backed data access
└── features/            each feature owns its BLoC
    ├── analytics/
    ├── gestures/
    ├── home/
    ├── onboarding/
    ├── reports/
    ├── vocabulary/
    └── quick_add/       voice + photo input
</code></pre>
<p>The data model is two tables and an inventory: <code>VocabularyItem</code>, <code>GestureItem</code>, and a <code>ChildProfile</code>. Everything else — frequency-of-use, age-at-first-production, comprehension-vs-production split — derives from those.</p>
<h2>Bilingual children</h2>
<p>This is where consumer apps fall over. If your kid says "dog" in English and "perro" in Spanish, those are not two vocabulary items — they're one concept. The MB-CDI norms only work if you count conceptual vocabulary, not surface form.</p>
<p>I baked that algorithm in. The doc is at <code>design/bilingual_logic.md</code> in the repo. The short version:</p>
<ol>
<li>Every MB-CDI item has a concept ID</li>
<li>Each child profile has 1–N spoken languages</li>
<li>A vocabulary entry is <code>(concept_id, language, age_at_first_production)</code></li>
<li>Comprehension and production are scored at the concept level, not the surface form</li>
</ol>
<p>That single decision means LexiGrow can give a bilingual kid a fair number against the norms instead of penalizing them for speaking two languages.</p>
<h2>What's hard about this</h2>
<p>The hardest part is <em>not over-medicalizing it</em>. Parents don't want a clinical assessment tool that makes them anxious about every milestone. The MB-CDI is a percentile system; it's normal for a 14-month-old to be at the 30th percentile and a 22-month-old to be at the 90th. LexiGrow has to communicate that without either downplaying real concerns or generating false alarms.</p>
<p>The current design is:</p>
<ul>
<li>Show progress against age-band norms, never against a single rigid line</li>
<li>Surface percentile ranges, not a single number</li>
<li>Explicitly flag the "this is a wide range" framing in onboarding</li>
<li>Provide a "share with your pediatrician" report — the only output styled like a clinical artifact</li>
</ul>
<p>If a parent wants the clinical output, they can ask for it. If they want the encouraging output, that's the default.</p>
<h2>What's next</h2>
<p>Phase 1 of the modernization plan adds integration tests. Phase 5 renames <code>voice/</code> to <code>quick_add/</code> and consolidates the photo + voice inputs. Phase 8 adds Firestore sync for multi-device families. After that, the obvious next thing is a longitudinal report — a year-by-year report card you can keep across siblings.</p>
<p>This was the first Flutter project I'd shipped to a place where it actually had to feel like a native iOS app. Material 3 helps. The remaining iOS-feels-different work is mostly haptics, swipe-back, and the photo picker — and the photo picker is half of why God invented <code>image_picker_ios</code>.</p>
<p>If you're a parent who wants to log first words properly, or a clinician who's tired of recommending tools that don't use the MB-CDI, the repo lives on my GitHub.</p>]]></content:encoded>
            <author>rpr2998@gmail.com (Raymond Riter)</author>
        </item>
        <item>
            <title><![CDATA[Traeger Peppered Beef Jerky]]></title>
            <link>https://raymondriter.dev/blog/making-homemade-beef-jerky</link>
            <guid>https://raymondriter.dev/blog/making-homemade-beef-jerky</guid>
            <pubDate>Fri, 22 Nov 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Taken from this Traeger recipe,&nbsp;<a href="https://www.traeger.com/recipes/peppered-beef-jerky">https://www.traeger.com/recipes/peppered-beef-jerky</a>.</p>
<p>Used eye of the round for the meat, hand sliced from whole Costco cut. Used dragons milk dark stout for the beer in marinade, and everything else listed in recipe which we doubled.</p>
<p>Marinated nearly 24hrs overnight. Dried out on paper towels hit with extra pepper.</p>
<img alt="Marinated raw beef jerky" loading="lazy" width="734" height="1305" decoding="async" data-nimg="1" style="color:transparent" srcset="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fjerkymarinated.d3f8c691.jpg&amp;w=750&amp;q=75 1x, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fjerkymarinated.d3f8c691.jpg&amp;w=1920&amp;q=75 2x" src="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fjerkymarinated.d3f8c691.jpg&amp;w=1920&amp;q=75">
<p>Finally 180° smoke for 4hrs 20mins on the Traeger. First time making my own and since it was warm, having warm beef jerky but we loved it.</p>
<img alt="Cooked beef jerky" loading="lazy" width="979" height="1305" decoding="async" data-nimg="1" style="color:transparent" srcset="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fcookedjerky.69113220.jpg&amp;w=1080&amp;q=75 1x, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fcookedjerky.69113220.jpg&amp;w=2048&amp;q=75 2x" src="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fcookedjerky.69113220.jpg&amp;w=2048&amp;q=75">]]></content:encoded>
            <author>rpr2998@gmail.com (Raymond Riter)</author>
        </item>
        <item>
            <title><![CDATA[RayFitnessPal: building the MyFitnessPal that MyFitnessPal refuses to build]]></title>
            <link>https://raymondriter.dev/blog/rayfitnesspal</link>
            <guid>https://raymondriter.dev/blog/rayfitnesspal</guid>
            <pubDate>Fri, 10 Apr 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I've used MyFitnessPal on and off since 2014. Every couple years the UX gets a little worse, the premium paywall a little more aggressive, and the food database a little more cluttered with phantom entries from people who don't know how a serving size works. So I built my own.</p>
<p>The official-sounding name is "Nutrition Tracking App." The disk-folder name is <code>myfitnesspal-clone</code>. The name I actually call it is RayFitnessPal.</p>
<h2>What it does</h2>
<ul>
<li><strong>Food logging</strong> — search, scan, or photograph a meal and get detailed nutrition</li>
<li><strong>AI food recognition</strong> — Gemini analyzes a photo and names what's on the plate, then macros get pulled from the actual nutrition databases</li>
<li><strong>Barcode scanning</strong> — point the phone, get the food, done</li>
<li><strong>Camera integration</strong> — meal photos saved against entries, plus AI-recognition source</li>
<li><strong>Real-time calculations</strong> — serving-size scaling that doesn't make you do arithmetic</li>
<li><strong>Meal planning</strong> — schedule future meals</li>
<li><strong>Water tracking</strong> — daily intake with reminders</li>
<li><strong>Recipe builder</strong> — combine ingredients into a custom recipe with computed nutrition</li>
<li><strong>Exercise integration</strong> — workouts and fitness activity logs</li>
<li><strong>Progress analytics</strong> — trends, weekly summaries, goal hit-rate</li>
<li><strong>Smart notifications</strong> — reminder cadence based on actual behavior</li>
<li><strong>Data export/import</strong> — your data is yours</li>
</ul>
<p>Crucially: <strong>dark mode that actually works</strong>, <strong>offline support</strong> via Dexie + localStorage, and <strong>real-time sync</strong> via Supabase for authenticated users.</p>
<h2>The stack</h2>
<pre class="language-text"><code class="language-text">Frontend     Next.js 15, React 19, TypeScript
Styling      Tailwind CSS
Backend      Supabase (Auth + Postgres + Realtime)
Food DB      USDA FoodData Central (free) + Nutritionix (free tier)
AI           Google Gemini (food recognition + Quick-Log)
Camera       WebRTC getUserMedia
Barcode      ZXing + HTML5 QR Code
Offline      Dexie (IndexedDB) + localStorage for preferences
</code></pre>
<p>The interesting choice here is going to free public APIs for the food database. The USDA FoodData Central is genuinely huge — about 1.9M entries — and it's a US government dataset, so it's not going behind a paywall. Nutritionix's free tier fills in the gaps for branded foods. Between them you get coverage that's competitive with MyFitnessPal Premium without any subscription.</p>
<h2>The Gemini Quick-Log trick</h2>
<p>The single feature that justifies the whole project is "Quick-Log." You point your phone at your plate, snap a photo, and a few seconds later you have a logged meal with macros within ~10% of correct.</p>
<p>The flow is:</p>
<ol>
<li>Capture the image via <code>getUserMedia</code></li>
<li>Send it to Gemini with a prompt that asks for a JSON list of <code>{food_name, estimated_grams}</code> per visible item</li>
<li>For each <code>food_name</code>, query Nutritionix for the closest match</li>
<li>Scale macros by <code>estimated_grams / standard_serving_grams</code></li>
<li>Sum, show the user, let them edit before saving</li>
</ol>
<p>The estimated_grams output from Gemini is genuinely accurate enough — within a slice of bread for most everyday meals. I don't trust it for restaurant portions, but for cooking-at-home it's better than my own eyeballing.</p>
<h2>What was hard</h2>
<p>The Firebase → Supabase migration. The original prototype was Firebase and the data model was wrong for relational queries (you can't really say "show me protein totals per week" cleanly in Firestore without denormalizing aggressively). Supabase + Postgres made every analytics query 3–5 lines instead of a custom Cloud Function. The data types are generated directly from the schema, so the TypeScript layer never drifts from the database.</p>
<p>Offline-first via Dexie was the second-hardest. Every log is staged locally first, then synced. The sync resolver has to handle: same-meal-edited-on-two-devices, item-deleted-on-one-device-but-not-the-other, and the "I logged 3 weeks of meals while on a camping trip with no service" case.</p>
<h2>What I'd build next</h2>
<p>A coach that actually understands my goals. The current "Quick-Log" tells me what's on the plate; it doesn't tell me whether I should be eating it given that I told it I'm trying to hit 180g protein this week. That's the next feature: a passive nudge layer driven by the same Gemini calls that already see every meal.</p>
<p>The codebase lives on my GitHub. It is enormously over-engineered for a personal nutrition app. That's the point.</p>]]></content:encoded>
            <author>rpr2998@gmail.com (Raymond Riter)</author>
        </item>
        <item>
            <title><![CDATA[rsbot: building an autonomous OSRS bot that actually has a plan]]></title>
            <link>https://raymondriter.dev/blog/rsbot</link>
            <guid>https://raymondriter.dev/blog/rsbot</guid>
            <pubDate>Thu, 04 Dec 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>The interesting thing about an Old School RuneScape bot is not the macro that clicks the tree. The macro is solved. What's not solved is <em>what to do next</em>.</p>
<p>Most bots run a single skill forever. Mine has a goal planner. You give it a long-term target — "combat to 70, total wealth to 1M GP, 200+ quest points" — and it picks its own activity based on current stats, inventory, gear, location, and how close it is to each goal. When it finishes one task, it picks the next one without me being there.</p>
<p>It's called rsbot. It's the largest active codebase I have on disk by source-file count (2,073 files). Most of those files are the verbose modular architecture, not bloat.</p>
<h2>The shape</h2>
<pre class="language-text"><code class="language-text">Entry:    run_autonomous.py
              │
              ▼
          BotEngine ──→ AutonomousPlayer (script)
              │
              ├─ DetectionBridge ──→ YOLOv8 pipeline
              ├─ AntiBan          ──→ humanlike timing
              ├─ GoalPlanner      ──→ long-term targets
              └─ TaskManager      ──→ short-term actions
</code></pre>
<p>Core layout:</p>
<pre class="language-text"><code class="language-text">bot/
├── core/                state machine, task manager, anti-ban, goal planner
├── modules/
│   ├── autonomous_combat.py
│   ├── autonomous_skilling.py     (woodcutting, mining, fishing,
│   │                               cooking, smithing, crafting)
│   ├── autonomous_banking.py
│   ├── autonomous_travel.py
│   ├── autonomous_questing.py
│   └── gearing_manager.py
├── ai/
│   ├── ai_brain.py                 LLM decision-making
│   └── strategic_advisor.py
├── game/                inventory, player, bank, login
└── input/               RuneLite plugin OR window-constrained mouse
</code></pre>
<p>Two input modes: a RuneLite plugin that posts intents over localhost, and a fallback pyautogui mode that constrains itself to a window rect. The latter is what you ship to a friend who won't install RuneLite plugins.</p>
<h2>Perception</h2>
<p>YOLOv8 does the heavy lifting. I trained it on the obvious classes — trees, ore rocks, fishing spots, monsters, bank booths, doors — using screenshots from my own gameplay. <code>mss</code> grabs frames; OpenCV preprocesses; YOLO outputs bounding boxes; a coordinate transform converts those to game-window pixels.</p>
<p><code>detection_bridge.py</code> is the layer that turns "raw detections" into "labeled entities I can decide about." It dedupes overlapping boxes, applies confidence thresholds per class, and tags entities with stable IDs across frames so the planner can say "the same chicken I was already attacking, not a different one."</p>
<h2>Strategy</h2>
<p><code>ai_brain.py</code> wraps a local LLM (LM Studio) for higher-level decisions. The planner doesn't ask the LLM what to <em>do</em> on every frame — that would be slow and wasteful. It asks the LLM when a decision is genuinely ambiguous: which of three nearby trees is best when one is closer but another is in safer territory; whether to bank or keep killing when the inventory is at 27/28; whether the current skill grind is the best one toward the goal.</p>
<p>The state machine handles everything that's mechanical. The LLM handles everything that's interesting.</p>
<h2>Goals, not scripts</h2>
<p>The thing I'm most proud of is the goal planner. You set it like this:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">goals</span><span class="token punctuation">:</span>
  <span class="token punctuation">-</span> <span class="token punctuation">{</span> <span class="token key atrule">type</span><span class="token punctuation">:</span> skill<span class="token punctuation">,</span>  <span class="token key atrule">skill</span><span class="token punctuation">:</span> combat<span class="token punctuation">,</span>  <span class="token key atrule">target</span><span class="token punctuation">:</span> <span class="token number">70</span> <span class="token punctuation">}</span>
  <span class="token punctuation">-</span> <span class="token punctuation">{</span> <span class="token key atrule">type</span><span class="token punctuation">:</span> wealth<span class="token punctuation">,</span> <span class="token key atrule">target_gp</span><span class="token punctuation">:</span> 1_000_000 <span class="token punctuation">}</span>
  <span class="token punctuation">-</span> <span class="token punctuation">{</span> <span class="token key atrule">type</span><span class="token punctuation">:</span> quest<span class="token punctuation">,</span>  <span class="token key atrule">target_qp</span><span class="token punctuation">:</span> <span class="token number">200</span> <span class="token punctuation">}</span>
</code></pre>
<p>Then <code>goal_planner.py</code> computes a heuristic ranking on every cycle:</p>
<pre class="language-text"><code class="language-text">score(activity) =
    expected_progress_per_minute(activity, current_state) /
        cost_of_setup(activity, current_state) *
        priority_weight(active_goal, activity)
</code></pre>
<p>The bot picks the highest-scoring activity, kicks off the appropriate module, and keeps going until the goal is achieved or a higher-scoring activity emerges.</p>
<p>That's why this isn't a fishing bot or a combat bot — it's "the bot for the goals you set." Combat to 70 might mean killing chickens to 30, hill giants to 50, and dust devils to 70. The planner makes those transitions on its own.</p>
<h2>State</h2>
<p>What works:</p>
<ul>
<li>Combat (recent successful run vs hill giants — full kill loop, prayer flick, loot pickup, restocks)</li>
<li>Skilling (woodcutting, mining, fishing, cooking, smithing, crafting all working)</li>
<li>Travel (route planner picks the optimal teleport/run path)</li>
<li>Antiban (humanlike timing distributions, mouse movement Bezier curves, idle injection)</li>
</ul>
<p>What's broken right now:</p>
<ul>
<li><strong>Banking.</strong> Debug logs from April show task timeouts on bank-opening — 40 minutes stuck on a gear-acquisition task that should take 30 seconds. There are five <code>bank_open_fail</code> screenshots saved as evidence. The current retry logic is too crude (15 retries on timeout); the fix is to detect "bank booth is occluded" vs "I clicked the wrong thing" and recover differently.</li>
<li>Questing is skeleton-only. <code>rune_mysteries.py</code> exists but isn't wired into the main loop.</li>
</ul>
<h2>What's next</h2>
<p>Three threads I'd pull, in order:</p>
<ol>
<li><strong>Fix banking.</strong> The whole goal-planner falls over when the bot can't reliably bank. Diagnose the failure modes from the saved screenshots, replace the retry loop with state-driven recovery.</li>
<li><strong>Wire questing into the main loop.</strong> Once questing is a primitive the planner can pick, the QP goals start working.</li>
<li><strong>Re-enable RL training.</strong> There's scaffolding at <code>rl/osrs_env.py</code> and <code>rl/combat_env.py</code> that's disabled. The OpenAI Gym wrapper around the game state would let me train a real RL agent for the combat sub-loop instead of hand-tuning prayer flicks.</li>
</ol>
<p>There's also a strong consolidation opportunity with <a href="https://github.com/Raymondriter">The Visual Bridge</a>, which solves the same vision problem from a different angle — it's an MCP server that wraps YOLOv8 + Moondream2 for any OSRS window. Visual Bridge could be the perception layer for rsbot, freeing the bot to focus on planning.</p>
<p>This is a project that taught me how complex an "agent that does one thing autonomously" actually is. Most days I'm not sure if I'm building software or breeding a creature.</p>]]></content:encoded>
            <author>rpr2998@gmail.com (Raymond Riter)</author>
        </item>
        <item>
            <title><![CDATA[The Tesla OSS suite: 85K words of research distilled into 8 repos]]></title>
            <link>https://raymondriter.dev/blog/tesla-oss-suite</link>
            <guid>https://raymondriter.dev/blog/tesla-oss-suite</guid>
            <pubDate>Fri, 01 May 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>I have a Model Y 2026 AWD on order. While I waited for delivery, I did what any sensible person would do: I built the eight pieces of software I'd want it to come with.</p>
<p>This started as a research dump. ~85K words of curated notes, 1,409 raw ideas pruned to 915, 40 clusters, three waves of buy-vs-build analysis. None of that ships. It's just kindling. What ships is a workspace called <code>tesla</code> with eight repos arranged in dependency order.</p>
<h2>The shape</h2>
<p>The library that everything else depends on comes first:</p>
<ul>
<li><strong>tesla-clip-tools</strong> — the shared library. Ten source plugins (Tesla, Wyze, Reolink, UniFi, Ring, Nest, Eufy, Frigate, Arlo, Blink), a sampler, generic VLM backends, and SEI primitives. Without this, every downstream repo would reimplement frame sampling and prompt scaffolding. v0.7.0, 135 tests.</li>
</ul>
<p>Two consumers prove the abstraction:</p>
<ul>
<li><strong>sentrytriage</strong> (v0.14.0, 115 tests) — local AI Sentry triage with a FastAPI dashboard, thumbs feedback, tune-prompt, A/B evaluator with train/test split, and embedded video. <code>pip install sentrytriage &amp;&amp; triage demo</code> boots a 30-second walkthrough.</li>
<li><strong>fsd-disengagement-studio</strong> (v0.5.0, 106 tests) — catalog and dashboard for FSD disengagements. Three paths: manual save, real-time Home Assistant trigger, and bulk SEI backfill. Per-driver splitter on top.</li>
</ul>
<p>Five independent repos cover orthogonal surfaces where the clip library wouldn't help:</p>
<ul>
<li><strong>hey-nabu-climate-concierge</strong> (v0.4.0 Python + v0.5.0 PWA, 33 + 62 tests) — DIY voice climate concierge. FastMCP server + Tesla-browser PWA + real Home Assistant WebSocket state subscription + IndexedDB history.</li>
<li><strong>teslakit</strong> (v0.1.0, 45 tests) — Docker Compose monorepo bundling Home Assistant + TeslaMate + Tesla HTTP Proxy + BLE bridge. Replaces Tessie's $13–20/month subscription.</li>
<li><strong>deliveryday-companion</strong> (v0.2.0, 54 tests) — delivery-day acceptance checklist with photos and a signed PDF report. Model Y 2026 AWD preset baked in (50 steps).</li>
<li><strong>tesla-changelog-diff</strong> (v0.3.0, GitHub-only, 61 tests) — per-VIN OTA diff bot. Three VLM backends (OpenAI, Anthropic, Gemini) for HMI screenshot diffs.</li>
<li><strong>guestkey-issuer</strong> (v0.1, Go scaffold) — the missing ~80-line primitive for <code>WhitelistOperation.addImpermanentKey</code> (<code>ROLE_GUEST=8</code>). Dry-run only for now.</li>
</ul>
<h2>Why these eight, in this order</h2>
<p>The constraint I set was that every repo had to be testable without a vehicle in hand. Synthetic seeds, mock Fleet API clients, deterministic fixtures, plugins for non-Tesla cameras. That constraint is the reason the OSS releases are portable to anyone — Tesla owner or not.</p>
<p><code>tesla-clip-tools</code> had to come first because everything downstream — Sentry triage, FSD disengagement classification, future plugins — needs the same source/sampler/VLM/SEI primitives. The two consumers exercise the abstraction. The five independents don't share a dependency graph; they just share a Tesla.</p>
<h2>The workspace itself</h2>
<p>There's a <code>doctor.py</code> at the root that runs every Python repo's test suite and prints a green/yellow/red dashboard. There's an <code>index.html</code> that renders the workspace overview with a Mermaid diagram, hero stats, and live-link cards — no build step, just open it. There's a <code>start-all-demos.{ps1,sh}</code> that boots three dashboards in parallel.</p>
<pre class="language-text"><code class="language-text">tesla/
├── tesla-clip-tools/         shared library (135 tests)
├── sentrytriage/             AI Sentry triage (115 tests)
├── fsd-disengagement-studio/ FSD disengagement catalog (106 tests)
├── hey-nabu-climate-concierge/ voice concierge (95 tests across Python + PWA)
├── teslakit/                 Tessie replacement (45 tests)
├── deliveryday-companion/    acceptance checklist (54 tests)
├── tesla-changelog-diff/     per-VIN OTA diff (61 tests)
├── guestkey-issuer/          Go scaffold for ROLE_GUEST=8
├── doctor.py                 workspace test runner
└── index.html                browser-rendered overview
</code></pre>
<h2>What I learned</h2>
<p>A few things only became obvious once I had eight things shipping side by side:</p>
<p><strong>The shared library should be the second thing you ship, not the first.</strong> I built <code>tesla-clip-tools</code> first as a library, but I didn't understand its real shape until I'd shipped <code>sentrytriage</code> against it. The right move is to ship <code>sentrytriage</code> from a single-file embedded version, then extract the library on the way to <code>fsd-disengagement-studio</code>.</p>
<p><strong>Synthetic seeds save the project.</strong> If <code>triage demo</code> didn't boot in 30 seconds with no Tesla and no cloud, nobody would ever try the thing. Determinism is a feature.</p>
<p><strong>Repo count &gt; monorepo for this kind of OSS.</strong> Each repo can be <code>pip install</code>'d on its own. Each has its own LICENSE (mostly MIT, AGPL for <code>teslakit</code> because of TeslaMate's contagion). Each can graduate independently.</p>
<p>When the Model Y actually arrives, the delivery checklist gets a real workout. Until then, the synthetic data keeps the repos honest.</p>]]></content:encoded>
            <author>rpr2998@gmail.com (Raymond Riter)</author>
        </item>
    </channel>
</rss>