<?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: Anshi Kandulna</title>
    <description>The latest articles on DEV Community by Anshi Kandulna (@anshi_kandulna_79eb00791a).</description>
    <link>https://dev.to/anshi_kandulna_79eb00791a</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%2F4008679%2F22e984d1-af00-441e-9ab3-72d0eb365237.png</url>
      <title>DEV Community: Anshi Kandulna</title>
      <link>https://dev.to/anshi_kandulna_79eb00791a</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anshi_kandulna_79eb00791a"/>
    <language>en</language>
    <item>
      <title>How We Built Motion Lore — AI Ballet Subtitles on AWS + Vercel</title>
      <dc:creator>Anshi Kandulna</dc:creator>
      <pubDate>Mon, 29 Jun 2026 21:34:06 +0000</pubDate>
      <link>https://dev.to/anshi_kandulna_79eb00791a/how-we-built-motion-lore-ai-ballet-subtitles-on-aws-vercel-4n50</link>
      <guid>https://dev.to/anshi_kandulna_79eb00791a/how-we-built-motion-lore-ai-ballet-subtitles-on-aws-vercel-4n50</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Ballet is beautiful. It's also completely unreadable if you didn't grow up with it.&lt;/p&gt;

&lt;p&gt;A viral YouTube Short changed how we thought about this. Hand-added captions translating dancer movements into plain language had people who'd &lt;em&gt;never&lt;/em&gt; cared about ballet suddenly hooked. Someone said they're autistic and this was the first time ballet made sense to them.&lt;/p&gt;

&lt;p&gt;We wanted to automate that. &lt;strong&gt;Motion Lore&lt;/strong&gt; generates synchronized narrative subtitles for ballet videos — drop in a YouTube URL or upload a file, get back a subtitle track that reads the choreography like a story.&lt;/p&gt;

&lt;p&gt;Here's how we built the backend on AWS + Vercel.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Two-Pass LLM Pipeline
&lt;/h3&gt;

&lt;p&gt;A single model looking at raw video footage with zero context produces mediocre subtitles. So we split it into two passes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Groq (llama-3.3-70b)&lt;/strong&gt; reads the video title, identifies the ballet, and enriches it with narrative context — characters, plot, setting.&lt;/li&gt;
&lt;li&gt;That context gets injected into &lt;strong&gt;Gemini 2.5 Flash's&lt;/strong&gt; system prompt &lt;em&gt;before&lt;/em&gt; it analyzes the video. Gemini isn't guessing blind anymore — it knows it's watching Giselle, not some random dance clip.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The quality difference is noticeable.&lt;/p&gt;

&lt;h3&gt;
  
  
  DynamoDB — Two Tables, Two Jobs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ballet-subtitles&lt;/code&gt;&lt;/strong&gt; is content-addressed by SHA-256 hash of the video URL or file. The same video uploaded by a thousand different users gets processed exactly once and cached forever. Zero redundant Gemini calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ballet-jobs&lt;/code&gt;&lt;/strong&gt; is our async job state machine — &lt;code&gt;queued → processing → done → failed&lt;/code&gt;. The key decision here was moving job state &lt;em&gt;out of memory&lt;/em&gt; and into DynamoDB early. In-memory state dies on restart and can't scale across instances. DynamoDB means our FastAPI backend is fully stateless.&lt;/p&gt;

&lt;h3&gt;
  
  
  S3 for Uploads
&lt;/h3&gt;

&lt;p&gt;User-uploaded videos land in S3 temporarily. Once Gemini processes them via the Gemini File API, they're deleted. YouTube URLs skip S3 entirely — Gemini has a native URL handler that takes them directly, which saved us an entire download pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  FastAPI on AWS
&lt;/h3&gt;

&lt;p&gt;Stateless, horizontally scalable, no server-side sessions. All state lives in DynamoDB. Spin up as many instances as needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vercel for the Frontend
&lt;/h3&gt;

&lt;p&gt;React frontend deployed on Vercel. The stateless FastAPI backend made this pairing clean — Vercel handles edge delivery and the frontend talks directly to the AWS backend over REST. No friction.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Tricky Parts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Gemini + S3&lt;/strong&gt;: Gemini can't pull from S3 directly. We had to route all uploads through the Gemini File API as an intermediate step — not obvious from the docs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Job state at scale&lt;/strong&gt;: First pass used in-memory job tracking. Worked fine locally, broke immediately under any real load or restart. DynamoDB fixed this completely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context poisoning&lt;/strong&gt;: Early on, bare file hashes were being passed straight to Gemini as identifiers. No title, no context. The subtitles came back generic and disconnected from the actual ballet. Adding the Groq classification gate — which fires before any Gemini call — fixed the quality problem at the root.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We'd Do Differently
&lt;/h2&gt;

&lt;p&gt;Honestly, we'd add the DynamoDB job state table on day one instead of migrating to it mid-build. The in-memory approach feels fine until it suddenly isn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Opera and classical Indian dance forms&lt;/li&gt;
&lt;li&gt;Community subtitle ratings to improve future generations&lt;/li&gt;
&lt;li&gt;Embeddable widget for ballet company websites&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Built for #H0Hackathon.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>vercel</category>
      <category>ai</category>
      <category>h0hackathon</category>
    </item>
  </channel>
</rss>
