<?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: Dearest</title>
    <description>The latest articles on DEV Community by Dearest (@dearest).</description>
    <link>https://dev.to/dearest</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%2F781242%2F123e58ff-b7f8-4821-9c0a-88cad3715b5b.jpeg</url>
      <title>DEV Community: Dearest</title>
      <link>https://dev.to/dearest</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dearest"/>
    <language>en</language>
    <item>
      <title>I Let an LLM Art-Direct Lyric Videos. Then I Took Most of the Job Away.</title>
      <dc:creator>Dearest</dc:creator>
      <pubDate>Thu, 22 Oct 2026 10:19:21 +0000</pubDate>
      <link>https://dev.to/dearest/i-let-an-llm-art-direct-lyric-videos-then-i-took-most-of-the-job-away-5a5n</link>
      <guid>https://dev.to/dearest/i-let-an-llm-art-direct-lyric-videos-then-i-took-most-of-the-job-away-5a5n</guid>
      <description>&lt;p&gt;I've been building &lt;a href="https://lyricvideogenerator.app" rel="noopener noreferrer"&gt;LyricVideoGenerator&lt;/a&gt;, a tool that takes a song and its lyrics and renders a lyric video where each word lights up as it's sung. Most of the people using it make songs with Suno or Udio and want something to post on YouTube that isn't a static cover image.&lt;/p&gt;

&lt;p&gt;The pitch is that you don't design anything. You drop in a song and get three finished looks to pick from. Somebody has to make those three looks. My first answer was "the LLM does it," and it didn't survive testing. This post is about what replaced it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Letting the LLM design the whole look worked, but took up to a minute and kept producing the same three designs. Now the LLM only labels the song (mood, genre, energy, chorus lines), and plain code picks three looks from a library of hand-built presets.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The renderer only reads JSON
&lt;/h2&gt;

&lt;p&gt;The video is a Remotion composition, and the only thing it reads is a &lt;code&gt;Style&lt;/code&gt; object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;focus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scroll&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;caption&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;palette&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;fg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;muted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;typography&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;700&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="nl"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uppercase&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="nl"&gt;align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;left&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;solid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gradient&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cover-blur&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;blur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;dim&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;dim&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;word-highlight&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;karaoke&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;line-fade&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
           &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;word-pop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;typewriter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;sections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;chorus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SectionStyle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;verse&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;SectionStyle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="c1"&gt;// plus emphasis words, a cover badge flag, a name and a one-line rationale&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So "design a lyric video" really means "fill in this object." That made it look like a perfect LLM job: a closed schema, a bounded set of choices, and taste as the only hard part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 1: the model designs everything
&lt;/h2&gt;

&lt;p&gt;The first prompt described what each field does on screen (the &lt;code&gt;focus&lt;/code&gt; template shows one line at 110px, &lt;code&gt;caption&lt;/code&gt; puts a subtitle block in the lower-left third, and so on) and asked for three complete Style objects per song.&lt;/p&gt;

&lt;p&gt;Zod validation alone wasn't enough. Only about 4% of calls failed on structure, but the output was still often unusable. The failures that mattered were about meaning.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fonts that don't exist.&lt;/strong&gt; &lt;code&gt;Big Shoulders Display&lt;/code&gt; showed up twice; Google Fonts merged it into &lt;code&gt;Big Shoulders&lt;/code&gt; a while ago. I ended up shipping a list of all 1,950 Google Fonts families and rejecting anything not on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Highlight you can't see.&lt;/strong&gt; For cover-based looks it would pull two near-white colours from the album art and use one for the lyrics and one for the highlighted word. On screen you couldn't tell which word was being sung.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Half an empty screen.&lt;/strong&gt; It paired the &lt;code&gt;caption&lt;/code&gt; template with a flat colour background. &lt;code&gt;caption&lt;/code&gt; exists to leave the top two-thirds of the frame to the cover art, so over a solid colour you get half a screen of nothing. That was 3 of 18 candidates in one round.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of those turned into a check: a font allowlist, contrast thresholds (lyrics at least 4.5:1 against whatever is actually behind them, the highlight colour at least 3:1), and "caption only over cover backgrounds." For background images I compute the real colour behind the text with &lt;code&gt;sharp&lt;/code&gt; (average and brightest colour of the cover, blended with the overlay at its &lt;code&gt;dim&lt;/code&gt; value) instead of trusting the model's &lt;code&gt;bg&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;The rule I held to: &lt;strong&gt;code validates and repairs, it never makes visual decisions.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;What the code does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Weight the font doesn't ship&lt;/td&gt;
&lt;td&gt;Snap to the nearest weight it does&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low contrast&lt;/td&gt;
&lt;td&gt;Heavier overlay first, then shift lightness, keep the hue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Emphasis word at the wrong index (off by one, twice)&lt;/td&gt;
&lt;td&gt;Look it up again by its text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anything else&lt;/td&gt;
&lt;td&gt;Send it back to the model with the error attached&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With that in place it worked. Across 6 songs × 3 runs, every run produced 3 valid candidates within one retry, and when I went through the 18 chorus frames, 15 were usable as-is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it still wasn't good enough
&lt;/h2&gt;

&lt;p&gt;Two problems that more validation couldn't fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency.&lt;/strong&gt; Three full designs took 40–60 seconds at the median and up to ~100 seconds. The cost was fine (two or three cents per song). Waiting a minute after uploading a song is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sameness.&lt;/strong&gt; Every song got its own three looks, but across songs the pattern was obvious. Over 54 candidates the template was locked to the slot: the first was always &lt;code&gt;focus&lt;/code&gt;, the third always &lt;code&gt;caption&lt;/code&gt;. The "bold" option was nearly always a dark gradient, centred heavy type and an amber highlight. Anyone making their third video would notice the AI has three moulds.&lt;/p&gt;

&lt;p&gt;I tried prompt work on the second problem. Removing hard-coded suggestions and asking for genre-specific choices raised font variety from 12 families over 18 candidates to 15 or 16, but the template-by-slot pattern never went away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 2: the model tags, code picks
&lt;/h2&gt;

&lt;p&gt;What runs in production now splits the job:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One cheap LLM call reads the lyrics and style tags and returns only labels: 1–3 moods, 1–2 genres, an energy level, which lines are the chorus, and 3–6 emphasis words.&lt;/li&gt;
&lt;li&gt;Code scores every hand-built preset against those labels.&lt;/li&gt;
&lt;li&gt;Code picks the best set of three under some diversity rules.&lt;/li&gt;
&lt;li&gt;Presets marked "from cover" pull their palette from the album art, and everything goes through the same validator as before.&lt;/li&gt;
&lt;/ol&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%2Fa6ub5ddkgy0mkup18eme.png" 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%2Fa6ub5ddkgy0mkup18eme.png" alt="Part of the preset library, every style on the same sample song" width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The scoring is deliberately boring. Mood counts most, then genre, then how close the energy level is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// weighted overlap: the first mood/genre the LLM lists counts most&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;picked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;picked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;
  &lt;span class="nx"&gt;picked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scorePreset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Preset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Analysis&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;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tags&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;a&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;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&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;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;genre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;genre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.6&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;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ENERGY_ORD&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;energy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;ENERGY_ORD&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;energy&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;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.45&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.35&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;e&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;Picking three is a brute-force loop over every triple. With 59 presets that's about 32,000 triples, which is nothing. A triple is only allowed if all three templates differ or all three animations differ, and if the song has cover art at least one pick has to use it. Triples that differ on both get a small bonus.&lt;/p&gt;

&lt;p&gt;What I gave up: the model no longer invents a look nobody has seen before.&lt;/p&gt;

&lt;p&gt;What I got: the slow part of the call is gone, every option has been checked by a human eye at least once, and variety now depends on the preset library. Growing that library is my job and doesn't depend on prompt luck.&lt;/p&gt;

&lt;p&gt;If the tagging call fails twice, a rules-only fallback picks from the style tags and the song's line density, so the user always gets three options.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;v1: LLM designs&lt;/th&gt;
&lt;th&gt;v2: LLM tags, code picks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LLM returns&lt;/td&gt;
&lt;td&gt;3 full &lt;code&gt;Style&lt;/code&gt; objects&lt;/td&gt;
&lt;td&gt;Moods, genres, energy, chorus lines, emphasis words&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who decides the look&lt;/td&gt;
&lt;td&gt;The model&lt;/td&gt;
&lt;td&gt;A score over hand-built presets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where variety comes from&lt;/td&gt;
&lt;td&gt;Prompt wording&lt;/td&gt;
&lt;td&gt;Size of the preset library&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;When the LLM fails&lt;/td&gt;
&lt;td&gt;Retry, or no video&lt;/td&gt;
&lt;td&gt;Rules-only fallback&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The LLM turned out to be good at the labelling. With section markers stripped out, it called "is this line part of the chorus" correctly on all 178 lines in my test set, twice. Full section labels were 97–98% right. The misses were bridge vs. pre-chorus confusions, which don't change anything visually, because the styling only cares whether a line is in the chorus.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the LLM still edits the design
&lt;/h2&gt;

&lt;p&gt;There's one place the model still touches the Style directly: "make the chorus bigger," "warmer colours," "less busy." That request goes to the model along with the current Style, and the model returns an RFC 6902 JSON Patch instead of a new object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"patch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"add"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/sections/chorus/typography/scale"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.35&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"explanation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Made the chorus lines larger so it lifts."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A patch keeps the edit minimal, and the change is easy to show and revert. Every patch is stored as a revision on the project. One gotcha: an &lt;code&gt;add&lt;/code&gt; fails if the parent object doesn't exist, and "add &lt;code&gt;/sections/chorus/typography/scale&lt;/code&gt; to a chorus with no typography yet" is exactly what the model is told to emit. So before applying the patch I create the missing parent objects. The patched result goes through the same validator, so a "make it moodier" request can't produce unreadable text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timing, briefly
&lt;/h2&gt;

&lt;p&gt;The word-by-word timing doesn't use an LLM at all. The audio and the lyric lines go to a forced-alignment API, which returns start and end times plus a per-word loss value. I map those timings back onto our lines by character offset and turn the loss into a 0–1 confidence with a logistic curve (calibrated so a loss of 1.8 maps to 0.6). Lines below 0.6 get flagged in the editor so you can nudge them by hand. Forced alignment is good, but a held note or a backing vocal can still throw it off.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell myself at the start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If an LLM's output is going to be rendered, write the semantic checks before the prompt. Valid JSON can still be an unreadable frame.&lt;/li&gt;
&lt;li&gt;"The model designs it" demos better than "the model picks from things a person designed." In daily use the second one is faster and repeats itself less.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to see the result, the free tier of &lt;a href="https://lyricvideogenerator.app" rel="noopener noreferrer"&gt;LyricVideoGenerator&lt;/a&gt; exports 720p 16:9 without an account.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>How I Built a Millisecond-Fast Squaredle Solver with Astro and React Islands</title>
      <dc:creator>Dearest</dc:creator>
      <pubDate>Tue, 05 Aug 2025 09:21:28 +0000</pubDate>
      <link>https://dev.to/dearest/how-i-built-a-millisecond-fast-squaredle-solver-with-astro-and-react-islands-26d3</link>
      <guid>https://dev.to/dearest/how-i-built-a-millisecond-fast-squaredle-solver-with-astro-and-react-islands-26d3</guid>
      <description>&lt;p&gt;Hey DEV community!&lt;/p&gt;

&lt;p&gt;For the last few months, I've been working on a passion project that I'm incredibly excited to finally share with you all. It started, as many projects do, from a personal frustration. I'm a big fan of the word puzzle game Squaredle, but I found the existing online solvers to be... lacking.&lt;/p&gt;

&lt;p&gt;I was stuck in a frustrating dilemma:&lt;/p&gt;

&lt;p&gt;Option A: A solver with an accurate, official dictionary, but a clunky, ad-filled UI that felt like it was designed in 2010 and was barely usable on mobile.&lt;/p&gt;

&lt;p&gt;Option B: A solver with a modern, slick interface, but an unofficial dictionary that frequently gave wrong answers or missed key words.&lt;/p&gt;

&lt;p&gt;I thought to myself: "Why can't we have both? Professional accuracy AND a delightful user experience?"&lt;/p&gt;

&lt;p&gt;So, I decided to build it myself. Today, I'm launching SquaredleSolver.org, and I wanted to share the technical journey of how I built it, particularly my choice of architecture.&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.amazonaws.com%2Fuploads%2Farticles%2Fm3p2ihyfgu1tw62msy8q.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fm3p2ihyfgu1tw62msy8q.png" alt=" " width="799" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Screenshots cannot display the visualized path. Please visit the website for the actual experience. &lt;a href="https://squaredlesolver.org/" rel="noopener noreferrer"&gt;https://squaredlesolver.org/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Goal: Performance First, Experience Always&lt;br&gt;
My primary goal was to create a solver that felt instantaneous. Word puzzles are about quick thinking, and the tool should match that pace. This meant I needed:&lt;/p&gt;

&lt;p&gt;A lightning-fast initial page load (First Contentful Paint).&lt;/p&gt;

&lt;p&gt;Highly interactive components that load only when needed.&lt;/p&gt;

&lt;p&gt;A non-blocking UI, especially during the heavy computation of solving the puzzle.&lt;/p&gt;

&lt;p&gt;A modern, clean, and mobile-first interface.&lt;/p&gt;

&lt;p&gt;This led me down a path of exploring different frameworks. While a standard React/Next.js app could work, I was concerned about the initial JavaScript payload. For a tool that needs to be fast from the get-go, sending a large bundle of JS upfront felt like the wrong approach.&lt;/p&gt;

&lt;p&gt;This is where Astro came in.&lt;/p&gt;

&lt;p&gt;The Architecture: Astro + React Islands 🏝️&lt;br&gt;
For those unfamiliar, Astro is a web framework designed for building content-rich, performance-first websites. Its killer feature is zero-JS-by-default. It renders your entire site to static HTML, and only ships JavaScript for the interactive components you explicitly specify.&lt;/p&gt;

&lt;p&gt;This concept of "islands of interactivity" was the perfect model for my solver.&lt;/p&gt;

&lt;p&gt;Here's how my page is structured:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Layout&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;header&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;header&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"prose"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GridInput&lt;/span&gt; &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="na"&gt;load&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ResultsPanel&lt;/span&gt; &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="na"&gt;visible&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Layout&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My page is mostly static content—the header, the instructions, the FAQ. This is pure HTML and CSS, which loads instantly. The interactive parts, like the grid input and the results panel, are React components treated as "islands."&lt;/p&gt;

&lt;p&gt;I used different client:* directives to control how these islands load:&lt;/p&gt;

&lt;p&gt;: The grid input is the main interactive element, so I load its JavaScript immediately after the page loads.&lt;/p&gt;

&lt;p&gt;: The results panel only needs to be interactive when the user scrolls down to see it. This directive lazy-loads the component's script until it enters the viewport.&lt;/p&gt;

&lt;p&gt;This architecture gave me the best of both worlds: the raw speed of a static site generator and the powerful UI capabilities of React, precisely where I needed them.&lt;/p&gt;

&lt;p&gt;Core Feature: A Non-Blocking Solver Engine&lt;br&gt;
The most computationally intensive part of the app is solving the grid. It involves traversing a Trie data structure with over 195,000 words and running a recursive search (DFS) across the grid.&lt;/p&gt;

&lt;p&gt;If I ran this on the main thread, the UI would freeze for a couple of seconds, creating a terrible user experience.&lt;/p&gt;

&lt;p&gt;The solution? Web Workers.&lt;/p&gt;

&lt;p&gt;When the user clicks "Solve," I spin up a new Web Worker to handle the entire process in the background:&lt;/p&gt;

&lt;p&gt;Main Thread: Packages the grid data and sends it to the worker.&lt;/p&gt;

&lt;p&gt;Worker Thread:&lt;/p&gt;

&lt;p&gt;Loads the pre-processed Trie dictionary.&lt;/p&gt;

&lt;p&gt;Executes the findAllWords search algorithm.&lt;/p&gt;

&lt;p&gt;Once complete, it posts the results (a list of found words) back to the main thread.&lt;/p&gt;

&lt;p&gt;Main Thread: Receives the results and updates the React state in the ResultsPanel to display the words.&lt;/p&gt;

&lt;p&gt;This ensures the UI remains completely responsive. The user can still scroll, click, and interact with the page while the puzzle is being solved.&lt;/p&gt;

&lt;p&gt;Here's a simplified look at the logic:&lt;/p&gt;

&lt;p&gt;TypeScript&lt;/p&gt;

&lt;p&gt;// In my SolverEngine.tsx React component&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSolve&lt;/span&gt; &lt;span class="o"&gt;=&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="nf"&gt;setIsLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&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;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/workers/solver.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentGrid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;dictionary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nwl2023&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;setSolvedWords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setIsLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;terminate&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Little Details: Styling and UX&lt;br&gt;
For styling, I chose Tailwind CSS. Its utility-first approach allowed me to build a clean, custom design system quickly without writing a single line of custom CSS. The JIT (Just-in-Time) compiler ensures that only the CSS classes I actually use are included in the final stylesheet, keeping it incredibly small.&lt;/p&gt;

&lt;p&gt;One of my favorite features is the path visualization. When you click a word in the results, it animates the path on the grid. This was a fun challenge involving:&lt;/p&gt;

&lt;p&gt;Calculating the precise coordinates of each grid cell.&lt;/p&gt;

&lt;p&gt;Dynamically generating an SVG overlay with smooth curves.&lt;/p&gt;

&lt;p&gt;Using CSS animations to "draw" the path.&lt;/p&gt;

&lt;p&gt;It turned a simple "cheat tool" into a learning companion.&lt;/p&gt;

&lt;p&gt;Final Thoughts and What's Next&lt;br&gt;
Building SquaredleSolver has been a fantastic learning experience. Choosing Astro was a game-changer for performance, and it forced me to think deliberately about which parts of my application truly needed to be interactive.&lt;/p&gt;

&lt;p&gt;This project reinforced a key lesson for me: the architecture you choose should directly serve the user experience you want to create. For a tool that needed to be fast, simple, and powerful, the Astro + React Islands model was a perfect fit.&lt;/p&gt;

&lt;p&gt;I've just launched it, and the journey is far from over. I'm already planning to add features like historical puzzle archives and user stats.&lt;/p&gt;

&lt;p&gt;I would be incredibly grateful for any feedback from the DEV community. Please give it a try and let me know what you think!&lt;/p&gt;

&lt;p&gt;Check it out here 👉 &lt;a href="https://squaredlesolver.org/" rel="noopener noreferrer"&gt;https://squaredlesolver.org/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>astro</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
