<?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>Forem: Jean Michael Mayer</title>
    <description>The latest articles on Forem by Jean Michael Mayer (@jeanmmayer).</description>
    <link>https://forem.com/jeanmmayer</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1217275%2F15947f82-c327-4584-978d-001e526a46e1.jpeg</url>
      <title>Forem: Jean Michael Mayer</title>
      <link>https://forem.com/jeanmmayer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/jeanmmayer"/>
    <language>en</language>
    <item>
      <title>Show Dev: A Dragon That Burns Your Village If You Misspell</title>
      <dc:creator>Jean Michael Mayer</dc:creator>
      <pubDate>Sun, 19 Apr 2026 20:35:05 +0000</pubDate>
      <link>https://forem.com/jeanmmayer/show-dev-a-dragon-that-burns-your-village-if-you-misspell-37db</link>
      <guid>https://forem.com/jeanmmayer/show-dev-a-dragon-that-burns-your-village-if-you-misspell-37db</guid>
      <description>&lt;h2&gt;
  
  
  The pitch
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://dragons-dictionary.edgecasefactory.com" rel="noopener noreferrer"&gt;The Dragon's Dictionary&lt;/a&gt;: a bilingual (English/Portuguese) word game where a hoarding dragon eats words you feed it. Spell them right, the dragon is pleased and adds them to its hoard. Spell them wrong, and it torches your village.&lt;/p&gt;

&lt;p&gt;It started as a joke about how Duolingo's owl is menacing but polite. I wanted something that felt genuinely unhinged — a creature that cares more about its word collection than your feelings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a dragon, why two languages
&lt;/h2&gt;

&lt;p&gt;I'm learning Portuguese. Most vocab apps treat bilingual learners like monolinguals who happen to own a dictionary. I wanted a game where switching languages mid-session is the point, not a settings toggle buried three menus deep.&lt;/p&gt;

&lt;p&gt;The dragon also solves a UX problem I keep hitting with language apps: stakes. Flashcards have no stakes. A burning village has stakes. The cognitive load of "don't kill the villagers" turns out to be a surprisingly effective mnemonic device.&lt;/p&gt;

&lt;h2&gt;
  
  
  The weird technical choices
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;It's almost entirely AI-generated.&lt;/strong&gt; Not "AI-assisted" — I mean the dragon's reactions, the villager names, the insults it hurls when you fail, are generated on demand. I keep a small corpus of base words and let the model riff on reactions per session. This is why the dragon feels inconsistent in a charming way: it is inconsistent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It runs in its own Railway service.&lt;/strong&gt; I have a small constellation of side projects under &lt;code&gt;edgecasefactory.com&lt;/code&gt;, and I used to cram them all into one monorepo behind one Next.js deployment. That was a mistake. One project's dependency bump would break two others. Now each dragon, factory, and whatever-else gets its own Railway service with its own lifecycle. Deploys take 40 seconds and I stopped dreading &lt;code&gt;npm update&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No database for game state.&lt;/strong&gt; The dragon's hoard lives in &lt;code&gt;localStorage&lt;/code&gt;. I considered Postgres, Redis, SQLite-on-a-volume, the works. Then I remembered this is a game about a dragon and nobody needs their village-burning history to sync across devices. Deleting the database was the single best architectural decision I made.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scoring loop
&lt;/h2&gt;

&lt;p&gt;Here's the core validation call. The model grades the word, returns a reaction, and decides the dragon's mood:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;feedDragon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/feed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hoard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getHoard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;villagersLost&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;addToHoard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;burnVillage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;villagersLost&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;reaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mood&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server-side check does two things: a dictionary lookup (cheap, deterministic) and an LLM call for the reaction text (expensive, flavorful). If the LLM times out, the dragon just grunts. Graceful degradation, dragon-style.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;p&gt;Portuguese has a lot of accented characters that are easy to get wrong on a US keyboard. I ended up adding a "close enough" mode because forcing learners to type &lt;code&gt;ção&lt;/code&gt; correctly on first try felt more like a bureaucratic form than a game. The dragon now accepts &lt;code&gt;cao&lt;/code&gt; but makes fun of you for it.&lt;/p&gt;

&lt;p&gt;Also: players burn their own village on purpose. A lot. I did not design for this. I now embrace it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;🐉 &lt;strong&gt;&lt;a href="https://dragons-dictionary.edgecasefactory.com" rel="noopener noreferrer"&gt;dragons-dictionary.edgecasefactory.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Feed the dragon. Don't feed it gibberish. Or do — the villagers signed waivers.&lt;/p&gt;

&lt;p&gt;Feedback welcome, especially from Portuguese speakers who want to tell me my conjugation tables are wrong.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>sideprojects</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Show Dev: Your Face Is Now a Music Generator</title>
      <dc:creator>Jean Michael Mayer</dc:creator>
      <pubDate>Sun, 19 Apr 2026 19:27:43 +0000</pubDate>
      <link>https://forem.com/jeanmmayer/show-dev-your-face-is-now-a-music-generator-4pm7</link>
      <guid>https://forem.com/jeanmmayer/show-dev-your-face-is-now-a-music-generator-4pm7</guid>
      <description>&lt;h2&gt;
  
  
  The Dumbest Idea I've Shipped This Month
&lt;/h2&gt;

&lt;p&gt;I built a thing called &lt;a href="https://mood-ring-playlist.edgecasefactory.com" rel="noopener noreferrer"&gt;The Mood Ring Playlist&lt;/a&gt;. It points your webcam at your face, reads your emotions, and generates a live music track that shifts as you do.&lt;/p&gt;

&lt;p&gt;Smile? Tempo kicks up, major key, bright pads. Scowl at your code review? Minor key, slower, a little more low end. Zone out completely? It drifts into ambient territory.&lt;/p&gt;

&lt;p&gt;This article is the "why would you do this" post, plus a couple of technical choices that turned out weirder than I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Build This
&lt;/h2&gt;

&lt;p&gt;I wanted to play with two things at once: browser-based face detection and procedural audio. Every "AI music" demo I've seen is either:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate a 30-second clip, wait, listen, regenerate.&lt;/li&gt;
&lt;li&gt;Upload a giant model to the browser and pray the user's laptop doesn't melt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I wanted something continuous. Not "click to generate a song" but "the song &lt;em&gt;is&lt;/em&gt; you, right now." That framing changed every architectural decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Face In The Browser, Music On The Server
&lt;/h2&gt;

&lt;p&gt;Face detection runs entirely client-side. Webcam frames → face landmarks → a small vector of emotion probabilities (happy, sad, angry, surprised, neutral, etc.). None of that leaves your machine. This is both a privacy win and a latency win — I don't want to ship webcam frames across the internet just to find out you're mildly annoyed.&lt;/p&gt;

&lt;p&gt;The music generation, though, lives in its own Railway service. That was deliberate. The main Next.js app is a thin frontend. The generator is a separate long-running Node service that holds state, manages the synthesis graph, and streams parameter updates based on the emotion vector it receives.&lt;/p&gt;

&lt;p&gt;Why split it?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cold starts would kill the vibe.&lt;/strong&gt; Serverless functions booting mid-song is not a vibe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's stateful.&lt;/strong&gt; The generator needs memory of what was just playing so transitions don't sound like someone slapping the radio dial.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I can redeploy the UI without killing the audio.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The client just posts emotion snapshots and receives back instructions for what the Web Audio API should do next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;emotions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/mood&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;emotions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tempo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;layers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;mixer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;tempo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;layers&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;emotions&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That 250ms debounce matters. Early versions updated every frame and the music sounded like a panic attack. Humans don't actually change mood 60 times a second — who knew.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI Part, Honestly
&lt;/h2&gt;

&lt;p&gt;The "AI" here isn't a giant model generating waveforms. It's a smaller system that maps an emotion vector to musical parameters: scale, tempo range, instrument layer weights, chord progression tendencies. Think of it as a learned policy over a procedural music engine, not Suno-in-a-box. That's how it stays real-time.&lt;/p&gt;

&lt;p&gt;I tried the "just call a big model every few seconds" route first. It sounded great when it worked and terrible when it stitched. Seams everywhere. The procedural-with-a-smart-director approach wins because continuity matters more than novelty in ambient music.&lt;/p&gt;

&lt;h2&gt;
  
  
  Known Weirdness
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If you wear glasses with heavy reflections, it thinks you're surprised a lot.&lt;/li&gt;
&lt;li&gt;If you laugh hard enough to close your eyes, it briefly assumes you left.&lt;/li&gt;
&lt;li&gt;Lighting matters more than I'd like. Sorry.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;Give it your face for a minute. See what you sound like today.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://mood-ring-playlist.edgecasefactory.com" rel="noopener noreferrer"&gt;mood-ring-playlist.edgecasefactory.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Headphones recommended. Webcam required. Judgment optional.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>webdev</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
