<?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: Mario Mihaila</title>
    <description>The latest articles on DEV Community by Mario Mihaila (@mvrix26).</description>
    <link>https://dev.to/mvrix26</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%2F4245723%2F400fb1fe-b63f-423b-bb18-72ccaca77280.jpg</url>
      <title>DEV Community: Mario Mihaila</title>
      <link>https://dev.to/mvrix26</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mvrix26"/>
    <language>en</language>
    <item>
      <title>Smart Cultural Lanterns: we taught Timișoara's street lamps to tell stories</title>
      <dc:creator>Mario Mihaila</dc:creator>
      <pubDate>Sun, 15 Nov 2026 10:50:41 +0000</pubDate>
      <link>https://dev.to/mvrix26/smart-cultural-lanterns-we-taught-timisoaras-street-lamps-to-tell-stories-5a57</link>
      <guid>https://dev.to/mvrix26/smart-cultural-lanterns-we-taught-timisoaras-street-lamps-to-tell-stories-5a57</guid>
      <description>&lt;h2&gt;
  
  
  What We Built
&lt;/h2&gt;

&lt;p&gt;The memories hidden in Timișoara’s squares were waiting to be told at UniHack 2026’s Immersive Arts track. We created Smart Cultural Lanterns – handcrafted lanterns equipped with an ESP32 board, a smart bulb and a speaker, placed in the cultural squares of the city. Each of the lanterns can be interacted with in two different ways: you can listen to the stories recorded by other people in the same place, or even record a new memory for that place and have the lantern read it out loud to other people walking through the square later.&lt;/p&gt;

&lt;p&gt;The lantern is the main character of this experience. Your phone is simply a remote control.&lt;/p&gt;

&lt;p&gt;The flow, end to end:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scan the QR code of the lantern fixed at location (&lt;code&gt;/f/unirii&lt;/code&gt;). The session recorded will be bound to the lantern. The URL is geofenced to location Uniirii square so you will be prompted to sign in when you are physically present in the square.&lt;/li&gt;
&lt;li&gt;Log in (Auth0) and start recording by pressing the button to upload the raw audio recordings (of whatever length) in whatever language you speak.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;single Gemini API call&lt;/strong&gt; will now in turn perform the 4 following actions on the newly uploaded raw audio: 1) transcription of the audio 2) content moderation (profanity/PII/spam –&amp;gt; reject or flag for human moderation) 3) language detection 4) translation of recorded story into Romanian and English.&lt;/li&gt;
&lt;li&gt;If the entry is approved, the backend generates two MP3s: one for your &lt;strong&gt;own cleaned-up voice&lt;/strong&gt; for the language you spoke in (more on that below), and one in ElevenLabs TTS for the translation.&lt;/li&gt;
&lt;li&gt;The story enters the lantern's play queue. The ESP32 polls the backend, downloads the MP3 over plain HTTP, sets the bulb's color over local UDP, and plays your story into the square.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The part we're proudest of is a small UX touch we call &lt;strong&gt;sealed stories&lt;/strong&gt;: after you submit, your story's text stays blurred behind a stardust animation on your phone. It only unseals once the physical lantern has actually spoken it. Your story belongs to the square before it belongs to your screen.&lt;/p&gt;

&lt;p&gt;The stack: &lt;strong&gt;FastAPI&lt;/strong&gt; + asyncpg on the backend, &lt;strong&gt;React + Vite&lt;/strong&gt; on the frontend, &lt;strong&gt;PostgreSQL (Tiger Data)&lt;/strong&gt; for storage and time-series presence analytics, &lt;strong&gt;Auth0&lt;/strong&gt; for identity, &lt;strong&gt;Gemini&lt;/strong&gt; for the audio pipeline, &lt;strong&gt;ElevenLabs&lt;/strong&gt; for voice, and an &lt;strong&gt;ESP32&lt;/strong&gt; with a MAX98357A I2S amp driving the speaker and a WiZ bulb.&lt;/p&gt;

&lt;h3&gt;
  
  
  What we learned the hard way
&lt;/h3&gt;

&lt;p&gt;A hackathon with real hardware on real networks teaches you things no tutorial will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An ESP32 on hotspot WiFi is a brutal code reviewer.&lt;/strong&gt; Our &lt;code&gt;/poll&lt;/code&gt; endpoint originally ran 4–5 sequential DB queries. Each round trip to the cloud database cost 200–500 ms, so the endpoint took up to ~4.8 s — and the firmware's HTTP timeout was 3 s. The lantern just saw &lt;code&gt;HTTP -11&lt;/code&gt; and went silent. The fix: collapse the whole poll (fetch next story, mark it, bump &lt;code&gt;last_seen&lt;/code&gt;, count presence) into &lt;strong&gt;one CTE statement&lt;/strong&gt;. One round trip, ~0.4 s, lantern happy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Networks fail in layers.&lt;/strong&gt; We lost two hours to a demo setup where &lt;em&gt;everything was configured correctly&lt;/em&gt; — except the laptop's hotspot IP had silently changed underneath all of it. And on my dev machine, HTTP/2 POSTs to Gemini's API hang forever while HTTP/1.1 sails through. When a request hangs with zero bytes received, suspect the protocol before your code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery guarantees aren't just for distributed-systems papers.&lt;/strong&gt; A story that gets lost between the database and a speaker in a public square is a person's memory, gone. So the queue became at-least-once: polling marks a story &lt;code&gt;delivering&lt;/code&gt;, the ESP32 must POST an ack after playback actually starts, and a sweeper requeues anything unacked after 75 s, up to 3 attempts.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;🌐 Live: &lt;strong&gt;&lt;a href="https://smart-lanterns.duckdns.org" rel="noopener noreferrer"&gt;smart-lanterns.duckdns.org&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Heads-up: the recording flow is GPS-geofenced to the actual squares in Timișoara — by design, you have to be there. The rest of the app is browsable from anywhere.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/A2lBVXA7ZvM" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1tcx69voyww9uwi8p4qd.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1tcx69voyww9uwi8p4qd.jpeg" alt="Lantern Telling a Story" width="768" height="1024"&gt;&lt;/a&gt;&lt;/p&gt;
The Smart Cultural Lantern in the square during testing



&lt;h2&gt;
  
  
  Partner Technologies
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Auth0&lt;/strong&gt; gates recording (you can listen anonymously, but your voice in a public square is attached to an identity). The integration itself via &lt;code&gt;@auth0/auth0-react&lt;/code&gt; was quick; the educational part was everything around it. Our WSL2 dev clock ran a few seconds behind real time, so freshly minted tokens failed validation with &lt;code&gt;ImmatureSignatureError&lt;/code&gt; — the &lt;code&gt;iat&lt;/code&gt; claim was "in the future." (Fix: a small &lt;code&gt;leeway&lt;/code&gt; in JWT validation.) And we silently bounced logins for an hour because the Auth0 application wasn't authorized for our API in the dashboard — the lesson being: surface auth errors in the UI immediately, never swallow them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Gemini&lt;/strong&gt; is the heart of the pipeline, and the design decision that paid off most was &lt;em&gt;one call instead of four&lt;/em&gt;. We send the raw audio with a prompt asking for a single JSON object: transcript, language, moderation verdict (&lt;code&gt;approved&lt;/code&gt; / &lt;code&gt;rejected&lt;/code&gt; / &lt;code&gt;pending_review&lt;/code&gt; with a reason), and translations. One call means one failure mode, one latency budget, and a moderation decision that actually has the full context of what was said. We hit one infrastructure quirk worth sharing: the official Python SDK's gRPC transport hung indefinitely on our network, so we talk to Gemini over plain REST with &lt;code&gt;httpx&lt;/code&gt; — about 30 lines, fully debuggable, zero magic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ElevenLabs&lt;/strong&gt; does two very different jobs. The obvious one: TTS for the translated version of each story. The less obvious one: &lt;strong&gt;audio isolation&lt;/strong&gt;. We decided a storyteller's &lt;em&gt;own voice&lt;/em&gt; should play for the language they spoke in — a grandmother's story about Piața Unirii should sound like her, not like a synthetic narrator. Street recordings from a phone are noisy, so originals go through ElevenLabs' audio-isolation model (with an ffmpeg &lt;code&gt;afftdn&lt;/code&gt; denoise fallback), then get loudness-normalized to −14 LUFS so TTS and human recordings sit at the same volume on the lantern's speaker. TTS is only the fallback for languages the storyteller didn't speak.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tiger Data (PostgreSQL)&lt;/strong&gt; holds everything: users, lanterns, stories, the play queue, and a time-series table of presence pings (the frontend pings every 15 s; the lantern shows how many people are "with it" right now, and the bulb's mood follows the crowd). Using raw asyncpg instead of an ORM felt risky under time pressure but turned out to be the enabler for the single-CTE poll endpoint — when you own the SQL, collapsing five queries into one is an evening's work, not a framework fight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hackathon Experience
&lt;/h2&gt;

&lt;p&gt;UniHack 2026 in Timișoara, Immersive Arts track. Our team split cleanly: my teammate owned the hardware — ESP32 firmware, I2S audio, driving the WiZ bulb over local UDP — and I took the frontend from "works" to "beautiful" (Romanian-first UI, dark mode, a recording fallback for iPhones, which of course record &lt;code&gt;audio/mp4&lt;/code&gt; instead of what every other browser produces), as well as the backend, the AI pipeline, and the deployment.&lt;/p&gt;

&lt;p&gt;The moment I'll remember: the first full end-to-end test. Phone in hand, standing over a lantern, recording a story — and a minute later hearing it come back out of the speaker &lt;em&gt;in the storyteller's own voice&lt;/em&gt;, with the bulb glowing. Everything between those two moments — the auth, the moderation, the translation, the queue, the firmware — had to work at once, and it did.&lt;/p&gt;

&lt;p&gt;The other thing hardware hackathons give you that pure-software ones don't: the bug is sometimes &lt;em&gt;physical&lt;/em&gt;. Not in your code, not in your config — in the clock of your VM, in the hotspot deciding you deserve a new IP, in a 3-second timeout compiled into a chip on the other side of the table. You learn to debug the whole system, down to the air.&lt;/p&gt;

&lt;p&gt;If you're ever in Timișoara: find a lantern, scan the code, and leave the square a story.&lt;/p&gt;

</description>
      <category>mlhacks</category>
      <category>devchallenge</category>
      <category>hackathon</category>
      <category>iot</category>
    </item>
  </channel>
</rss>
