<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alan De Bortolo</title>
    <description>The latest articles on DEV Community by Alan De Bortolo (@alandebortolo).</description>
    <link>https://dev.to/alandebortolo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4213353%2F7164ba4a-8c84-4a23-8dc3-32366c9065db.png</url>
      <title>DEV Community: Alan De Bortolo</title>
      <link>https://dev.to/alandebortolo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alandebortolo"/>
    <language>en</language>
    <item>
      <title>Ofrenda Local: an offline ofrenda builder, and why a grandmother's sayings need one language per phrase</title>
      <dc:creator>Alan De Bortolo</dc:creator>
      <pubDate>Wed, 28 Oct 2026 15:22:46 +0000</pubDate>
      <link>https://dev.to/alandebortolo/ofrenda-local-my-grandmothers-sayings-survived-the-transcript-only-after-i-stopped-trusting-one-18k0</link>
      <guid>https://dev.to/alandebortolo/ofrenda-local-my-grandmothers-sayings-survived-the-transcript-only-after-i-stopped-trusting-one-18k0</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week4-2026-10-26"&gt;Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Ofrenda Local takes a voice recording of someone telling a story about a relative who passed away and turns it into an ofrenda card: the foods they cooked, the songs they sang, the places they lived, the exact things they used to say, and a short remembrance written in the speaker's voice. It also saves the transcript and a &lt;code&gt;story.md&lt;/code&gt;, so the story is kept as text and not only as a pretty page.&lt;/p&gt;

&lt;p&gt;Everything runs on the laptop. Transcription is whisper.cpp, the writing is Gemma 3 4B on llama.cpp, and the glue is one Python file with no dependencies outside the standard library.&lt;/p&gt;

&lt;p&gt;I read the theme as "the memories and stories we carry with us". The ghost in this machine is the person in the recording. The machine should help keep them, and it must never put words in their mouth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Live card generated from the sample recording: &lt;a href="https://designmaster-solutions.github.io/ofrenda-local/" rel="noopener noreferrer"&gt;https://designmaster-solutions.github.io/ofrenda-local/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The sample (&lt;code&gt;samples/abuela-rosa.m4a&lt;/code&gt;) is synthetic: I wrote a short story about a fictional grandmother, Rosa Delgado, and voiced it with the macOS &lt;code&gt;say&lt;/code&gt; voices, Samantha for the English and Paulina for her Spanish sayings. Full run on my Mac, from audio to HTML: about 16 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/DesignMaster-Solutions" rel="noopener noreferrer"&gt;
        DesignMaster-Solutions
      &lt;/a&gt; / &lt;a href="https://github.com/DesignMaster-Solutions/ofrenda-local" rel="noopener noreferrer"&gt;
        ofrenda-local
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Turn a recorded family story into a Día de los Muertos ofrenda, fully offline (whisper.cpp + Gemma)
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Ofrenda Local&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Record a relative telling a story about someone who passed away. Ofrenda Local turns that recording into an
ofrenda card (favorite foods, songs, places, the things they used to say, and a short remembrance) &lt;strong&gt;without
anything leaving your computer&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Built for the DEV Hacktoberfest Open-Source AI Challenge, week 4: &lt;em&gt;Ghost in the Machine&lt;/em&gt;.&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;story.m4a ──ffmpeg──► 16 kHz wav ──silence cuts──► phrases ──whisper.cpp (language per phrase)──► transcript
transcript ──Gemma 3 4B on llama.cpp (JSON-schema grammar)──► card ──grounding filter──► ofrenda.html + story.md
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Run it&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;brew install llama.cpp whisper-cpp ffmpeg
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; whisper model: ggml-small.bin from https://huggingface.co/ggerganov/whisper.cpp&lt;/span&gt;
llama-server -hf ggml-org/gemma-3-4b-it-GGUF --port 18099 -c 4096 &lt;span class="pl-k"&gt;&amp;amp;&lt;/span&gt;
python3 ofrenda.py samples/abuela-rosa.m4a -o out          &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; transcribe + build the card&lt;/span&gt;
python3 ofrenda.py samples/abuela-rosa.m4a -t out/transcript.txt -o out   &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; after fixing names by hand&lt;/span&gt;
open out/ofrenda.html&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Env vars: &lt;code&gt;OFRENDA_LLM&lt;/code&gt; (any OpenAI-compatible endpoint that supports &lt;code&gt;json_schema&lt;/code&gt;), &lt;code&gt;OFRENDA_WHISPER&lt;/code&gt; (model path)
Python standard library only. Tests: &lt;code&gt;python3&lt;/code&gt;…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/DesignMaster-Solutions/ofrenda-local" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;story.m4a -&amp;gt; ffmpeg -&amp;gt; 16 kHz wav -&amp;gt; cut at pauses -&amp;gt; whisper.cpp, language detected per phrase -&amp;gt; transcript
transcript -&amp;gt; Gemma 3 4B on llama-server, JSON schema compiled to a grammar -&amp;gt; card -&amp;gt; grounding filter -&amp;gt; ofrenda.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four things turned out to matter more than the model choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Whisper picks one language per file.&lt;/strong&gt; My first transcript read: &lt;em&gt;"Whenever we complained, she used to say, And when we left her house she always said, I miss her hands..."&lt;/em&gt;. Both Spanish sayings were gone. Whisper had detected English for the whole file and silently dropped the parts that were not English. Those sayings are the most important part of an ofrenda, and a family where grandma switches languages mid-sentence is the normal case, not the edge case.&lt;/p&gt;

&lt;p&gt;The fix: run ffmpeg's &lt;code&gt;silencedetect&lt;/code&gt; to find pauses, cut the audio there, and transcribe each phrase with &lt;code&gt;-l auto&lt;/code&gt;. Now the sayings come through: &lt;em&gt;"Con paciencia y saliva, el elefante se comió a la hormiga."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Short phrases lose names.&lt;/strong&gt; Cutting fixed the language but broke the places: Oaxaca became "Wahaka" and Tlacolula became "Tulicalula", because a two-second clip has no context. whisper.cpp takes a &lt;code&gt;--prompt&lt;/code&gt;, so each phrase now gets the last 400 characters of the story so far. Oaxaca and Tlacolula came back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Grammar beats begging.&lt;/strong&gt; With Gemma 3 1B the "remembrance" field came back as &lt;code&gt;":["&lt;/code&gt;. Instead of a longer prompt, I gave llama-server a JSON schema (&lt;code&gt;response_format: json_schema&lt;/code&gt;). llama.cpp compiles it into a grammar, so the model cannot produce anything but the card's shape, and &lt;code&gt;minLength: 120&lt;/code&gt; on the remembrance means it cannot stop after two characters. I moved to Gemma 3 4B for the writing quality; the grammar is what made the output reliable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The model will invent a grandfather.&lt;/strong&gt; While testing I passed an empty transcript by mistake. Gemma returned a complete, touching remembrance for "Abuelo Ricardo", who loved pan dulce and La Bamba. Nobody had said a word. For a tool that writes about real people who died, that is the worst possible bug. So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an empty or very short transcript is refused before the model is called;&lt;/li&gt;
&lt;li&gt;every food, song, place and saying must share words with the transcript or it is dropped;&lt;/li&gt;
&lt;li&gt;if the card's name is not in the story, the run fails.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same rule shaped the remembrance prompt. An early card ended with "It's a shame she moved from Oaxaca to San Antonio". Nobody said that either, so the prompt now asks for no opinions, and the code keeps only the three sentences it asked for, because a small model under a length constraint sometimes keeps going and starts echoing the prompt.&lt;/p&gt;

&lt;p&gt;There are unit tests for the grounding filter and the HTML escaping, and I watched them fail with the filter disabled before trusting them.&lt;/p&gt;

&lt;p&gt;Whisper still hears "tamales de rojas" instead of "rajas" and "Pedro in Fonte" instead of Pedro Infante. I did not try to make the model guess those. The transcript is saved next to the card, a family member fixes the four words, and &lt;code&gt;python3 ofrenda.py story.m4a -t out/transcript.txt&lt;/code&gt; rebuilds the card from the corrected text. The family corrects, the model doesn't guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Because of what is in these recordings. A grandmother's voice, the names of everyone in the family, where they lived, the illness nobody talks about. That is not something I want to upload to a service so it can be turned into a card, and it is exactly the kind of recording families keep for fifty years. Running whisper.cpp and Gemma locally means the audio never leaves the machine, it works in a kitchen in Oaxaca with no internet, and it costs nothing per story.&lt;/p&gt;

&lt;p&gt;Open also meant I could change the parts a closed API would not let me touch: transcribe per phrase with my own cut points, feed whisper its own running context, and constrain the model with a grammar instead of hoping it follows instructions. And in thirty years, when the services of today are gone, &lt;code&gt;llama-server&lt;/code&gt; plus a GGUF file will still run the same story the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Spirit of It
&lt;/h2&gt;

&lt;p&gt;The last line of the sample is "I miss her hands, and her kitchen that always smelled like cinnamon." The card does not need to be clever. It needs to keep the champurrado, the Pedro Infante on the radio, and "Ve con Dios, mijo" exactly as she said it, and not add a single thing she didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;Best Use of Gemma: Gemma 3 4B (and 1B during development) runs locally through llama.cpp and writes every card, constrained by a JSON-schema grammar.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
