<?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: visiohex</title>
    <description>The latest articles on DEV Community by visiohex (@jy_wang_2c4d36f4d66adea80).</description>
    <link>https://dev.to/jy_wang_2c4d36f4d66adea80</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%2F3765913%2F6890e795-c29f-462e-a420-54ad45b80086.png</url>
      <title>DEV Community: visiohex</title>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jy_wang_2c4d36f4d66adea80"/>
    <language>en</language>
    <item>
      <title>Why I don't automatically retry timed-out AI video requests</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Sun, 01 Nov 2026 15:11:15 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/why-i-dont-automatically-retry-timed-out-ai-video-requests-fhh</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/why-i-dont-automatically-retry-timed-out-ai-video-requests-fhh</guid>
      <description>&lt;p&gt;A request times out. The obvious next step is to try again.&lt;/p&gt;

&lt;p&gt;For a video generation request, that can be an expensive guess. The provider may already be rendering the first video. You just didn't receive its task ID.&lt;/p&gt;

&lt;p&gt;I'm working on &lt;a href="https://aieffectstudio.com/" rel="noopener noreferrer"&gt;AI Effect Studio&lt;/a&gt;, where people choose an effect, upload photos, and get a short video. The generation backend has to deal with this gap between sending a paid request and knowing what happened to it.&lt;/p&gt;

&lt;p&gt;Here's how the current implementation handles it, including the part that still needs manual review.&lt;/p&gt;

&lt;h2&gt;
  
  
  The timeout doesn't tell you enough
&lt;/h2&gt;

&lt;p&gt;Imagine this sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Our server sends a generation request.&lt;/li&gt;
&lt;li&gt;The provider accepts it and starts work.&lt;/li&gt;
&lt;li&gt;The connection drops before our server receives the response.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From our side, the request failed. From the provider's side, there's a video being generated.&lt;/p&gt;

&lt;p&gt;Sending the same payload again doesn't necessarily resume that work. Unless the provider supports a documented idempotency mechanism, it may create another paid task.&lt;/p&gt;

&lt;p&gt;Exponential backoff changes when that second request happens. It doesn't make the second request safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record the attempt before sending it
&lt;/h2&gt;

&lt;p&gt;Each local job stores a submission timestamp separately from its provider task ID. Before calling the generation API, the worker records that it's about to submit.&lt;/p&gt;

&lt;p&gt;That gives the next worker enough information to distinguish these cases:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What the job has&lt;/th&gt;
&lt;th&gt;What the worker does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No submission marker and no provider ID&lt;/td&gt;
&lt;td&gt;Claim the job, record the attempt, then submit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A provider ID&lt;/td&gt;
&lt;td&gt;Query that existing task&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A submission marker but no provider ID&lt;/td&gt;
&lt;td&gt;Hold the job for support review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An output URL already recorded&lt;/td&gt;
&lt;td&gt;Continue processing or saving that video&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The database updates are conditional, and processing uses a short lease so overlapping workers don't casually pick up the same job. The submission marker is checked and written in the database before the external call.&lt;/p&gt;

&lt;p&gt;There's an awkward consequence here. If the worker crashes after writing the marker but before sending the request, the job also ends up waiting for review. It might never have reached the provider.&lt;/p&gt;

&lt;p&gt;I accept that tradeoff for the expensive generation step. The marker prevents an automatic second submission, but it cannot prove that the first one happened. This is a conservative fallback, and it leaves some real work for support.&lt;/p&gt;

&lt;p&gt;A provider-side idempotency key would be a better way to close that gap. A local timestamp can't give us exactly-once execution across two systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retry the stage that failed
&lt;/h2&gt;

&lt;p&gt;A video job keeps going after the model finishes. Depending on the effect, there's a soundtrack step, followed by saving the output to our own storage.&lt;/p&gt;

&lt;p&gt;If storage fails, generating another video would be a costly way to retry a file transfer. The job records the source video URL so recovery can continue from the output it already has.&lt;/p&gt;

&lt;p&gt;The soundtrack step has a different retry policy. It's much cheaper than generation, so it gets bounded retries. If those are exhausted, the delivery path can fall back to the silent footage. That is a product compromise: the user gets a video, but the soundtrack may be missing. The interface and product description need to make that possibility clear.&lt;/p&gt;

&lt;p&gt;Credits need the same care. A confirmed generation failure restores the user's allowance. An ambiguous submission stays reserved while it's investigated, because a timeout alone doesn't tell us whether paid work is still running.&lt;/p&gt;

&lt;p&gt;The question I use when reviewing these paths is pretty mundane: if this line runs twice, do we check the same work, or buy more work?&lt;/p&gt;

&lt;p&gt;Polling an existing task, copying an existing output, and starting a fresh render deserve different answers. Putting all three behind one generic retry helper would hide the distinction I most need to see.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>backend</category>
      <category>api</category>
    </item>
    <item>
      <title>Building the photo upload form for Tiny Desk AI</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Sun, 01 Nov 2026 14:31:10 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/building-the-photo-upload-form-for-tiny-desk-ai-359i</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/building-the-photo-upload-form-for-tiny-desk-ai-359i</guid>
      <description>&lt;p&gt;The upload form in &lt;a href="https://tiny-desk-ai.app/" rel="noopener noreferrer"&gt;Tiny Desk AI&lt;/a&gt; has three possible seats: lead singer, sax player, and keyboard player. You choose Solo, Duo, or Trio, then add one photo per person. The output is a short AI-generated desk-concert clip with a fixed sequence.&lt;/p&gt;

&lt;p&gt;I'm working on this project. This post is about the photo form, because even with only three inputs, there are a few ways to lose someone's choices or assign a photo to the wrong seat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give each photo a role
&lt;/h2&gt;

&lt;p&gt;The form stores photos by role:&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;const&lt;/span&gt; &lt;span class="nx"&gt;CAST_ROLES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lead&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sax&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keys&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="kd"&gt;as const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CastRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;CAST_ROLES&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Photos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CastRole&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;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 selected cast size controls which roles are visible:&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;const&lt;/span&gt; &lt;span class="nx"&gt;seats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;CAST_ROLES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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="nx"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That leaves one ordering to use for rendering the inputs and building the upload list. The singer stays the singer when someone switches from Trio to Solo and back.&lt;/p&gt;

&lt;p&gt;Switching the cast size doesn't delete the hidden photos. Removing a photo is a separate action. Someone can compare the options without having to pick the same files again.&lt;/p&gt;

&lt;p&gt;There is a limit to this design: each cast is a prefix of the same list. It works for this product because Duo always means lead plus sax. If we added a lead-and-keyboard option, a count alone would no longer describe the selection.&lt;/p&gt;

&lt;h2&gt;
  
  
  A thumbnail isn't the upload
&lt;/h2&gt;

&lt;p&gt;Each selected file gets an object URL for its preview. The component revokes that URL when the file changes or the component unmounts.&lt;/p&gt;

&lt;p&gt;The form also checks that the browser can decode the image, then checks its dimensions and aspect ratio. A file picker accepting JPEG, PNG, and WebP only helps with selection; it doesn't prove that the file is usable. These browser checks give earlier feedback. Server-side validation still has to protect the upload endpoint.&lt;/p&gt;

&lt;p&gt;After a successful upload, the form remembers both the &lt;code&gt;File&lt;/code&gt; object and the returned upload ID for that role. On another submission attempt, an unchanged file can reuse its ID. Choosing a replacement clears the cached upload for that seat.&lt;/p&gt;

&lt;p&gt;That distinction matters when one upload succeeds and a later one fails. There is no reason to send the first photo again while the page still has its successful upload recorded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Restoring a draft can race with the user
&lt;/h2&gt;

&lt;p&gt;Selected files are saved in IndexedDB. Session storage holds a reference for each role, and the saved records have a 24-hour validity window. This is local recovery in the current browser session, with no cross-device promise.&lt;/p&gt;

&lt;p&gt;Loading those files is asynchronous. A person can select a new photo before restoration finishes. If the old draft then overwrites the new selection, the form has undone their work.&lt;/p&gt;

&lt;p&gt;The component keeps a choice counter. Restoration only applies while that counter is still zero. Once someone starts choosing photos, their current choices take precedence.&lt;/p&gt;

&lt;p&gt;The storage helper also waits for the IndexedDB transaction to complete before resolving a save. A successful &lt;code&gt;put&lt;/code&gt; request alone isn't enough: the transaction can still abort.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the form can't promise
&lt;/h2&gt;

&lt;p&gt;A clear preview and a correct role assignment don't guarantee a good generated video. Faces can drift, especially through motion. The homepage shows the input photos alongside sample results and explains which parts of the clip are fixed, so people can judge the format before paying.&lt;/p&gt;

&lt;p&gt;The frontend has a narrower job: keep the selected photos in the right seats, explain invalid inputs, and preserve work where it can. For this form, those details deserve as much attention as the generate button.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
    </item>
    <item>
      <title>Angry Dance Filter AI: Engineering a Fixed-Format Video Workflow</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Tue, 27 Oct 2026 15:56:00 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/angry-dance-filter-ai-engineering-a-fixed-format-video-workflow-2olm</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/angry-dance-filter-ai-engineering-a-fixed-format-video-workflow-2olm</guid>
      <description>&lt;p&gt;A one-photo video tool has a small interface, but a surprisingly demanding backend. The user uploads a picture, pays, and expects a finished file. Behind that interaction are several separate questions: what exactly was promised, was a generation request accepted, and is the returned video safe to deliver?&lt;/p&gt;

&lt;p&gt;This is a technical walkthrough of decisions in my project, &lt;a href="https://angrydancefilterai.com/" rel="noopener noreferrer"&gt;Angry Dance Filter AI&lt;/a&gt;. It turns a photo into a specific kitchen dance routine. The useful engineering details are the boundaries around the generation request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with one defined output
&lt;/h2&gt;

&lt;p&gt;The product accepts one photo and targets a 14-second, vertical 9:16, 720p video with audio. The routine and kitchen come from a reference clip; the photo supplies the person's appearance. Users do not write a prompt or choose a different background.&lt;/p&gt;

&lt;p&gt;That narrow scope makes the output contract concrete. In the TypeScript implementation, it is represented as:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PUBLIC_OUTPUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;durationSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;aspectRatio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;9:16&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;resolution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;720p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;audio&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="p"&gt;}&lt;/span&gt; &lt;span class="kd"&gt;as const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Runtime validation checks deployment settings against that contract. If they disagree, new generation is unavailable rather than silently selling a different format.&lt;/p&gt;

&lt;p&gt;This matters beyond the model call. A resolution change also affects checkout text, emails, translated copy, and documentation. A shared contract reduces drift, although prose that repeats those values still needs review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let the reference clip carry the motion
&lt;/h2&gt;

&lt;p&gt;The generation request uses NanoAPI's Genjutsu model in motion-control mode. The server builds the request from a signed photo URL, a reference video URL, a server-owned prompt, an explicit resolution, and a webhook URL.&lt;/p&gt;

&lt;p&gt;In this integration, the reference clip supplies the length, framing, motion, and audio. There is no separate duration or audio parameter in the task body, and no extra audio-merging stage in this path.&lt;/p&gt;

&lt;p&gt;The tradeoff is limited creative control. The same scene and routine are part of the product contract. It also does not make identity preservation deterministic: faces, clothing details, and hands can still drift. The interface needs to explain those limits before someone uploads a photo.&lt;/p&gt;

&lt;h2&gt;
  
  
  A timeout is an unknown outcome
&lt;/h2&gt;

&lt;p&gt;The most consequential failure case happens when the provider might have accepted a paid request, but the response never reaches the application.&lt;/p&gt;

&lt;p&gt;Automatically repeating that POST can create a second task. Returning the user's credit immediately can also be wrong if the original task is still running.&lt;/p&gt;

&lt;p&gt;The job processor therefore records a submission marker before sending the request and persists the exact provider request body. Once a task ID is available, subsequent processing reads that task instead of creating another one.&lt;/p&gt;

&lt;p&gt;If submission was attempted but no task ID was recovered, the job is held for support review. The allowance remains reserved. A definitely rejected request follows a different path and can restore the allowance.&lt;/p&gt;

&lt;p&gt;This is a deliberate tradeoff: an ambiguous job may need manual reconciliation. The marker does not create exactly-once delivery across two systems. In particular, a crash between recording the marker and sending the request can leave a job needing review even though nothing reached the provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat webhooks as a reason to check
&lt;/h2&gt;

&lt;p&gt;The callback handler verifies a signature over the raw request body before parsing it. It also bounds the body size and checks the timestamp.&lt;/p&gt;

&lt;p&gt;After validation, it uses the task ID to locate the local job and trigger processing. It does not trust a callback's success flag or output URL as the final result. The processor fetches the task through the provider API and checks its ID and model before advancing the job.&lt;/p&gt;

&lt;p&gt;Signature verification and an authoritative task read solve different problems. The first authenticates the notification; the second determines the current task state. Neither replaces guarded updates in the local job processor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make failure behavior explicit in tests
&lt;/h2&gt;

&lt;p&gt;The repository includes regression cases for ambiguous submissions, repeated processing, request snapshots, invalid webhook signatures, and mismatched task IDs or models.&lt;/p&gt;

&lt;p&gt;One particularly useful assertion is that a simulated network timeout followed by another processing pass still produces only one provider submission. Another supplies a signed callback claiming success while the task API reports failure; the task API state wins.&lt;/p&gt;

&lt;p&gt;For this kind of application, the small upload form is only the entrance. The design work is in preserving the output contract and distinguishing confirmed failure from uncertainty, without quietly creating another paid operation.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Day 1 of Owning a Roadster V2: A One-Photo Video Generator</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Sun, 18 Oct 2026 03:42:53 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/day-1-of-owning-a-roadster-v2-a-one-photo-video-generator-176a</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/day-1-of-owning-a-roadster-v2-a-one-photo-video-generator-176a</guid>
      <description>&lt;p&gt;I built &lt;a href="https://day1ofowningaroadster.com/" rel="noopener noreferrer"&gt;Day 1 of Owning a Roadster&lt;/a&gt;, a small photo-to-video site for one specific clip: a driver in a white Roadster gets stuck in rainy traffic, hits a button, and takes off. There is a loop in the air and a cup of coffee that does not survive the ride.&lt;/p&gt;

&lt;p&gt;You upload a photo of yourself or your pet. The site uses that photo for the driver and keeps the Roadster V2 sequence fixed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the input small
&lt;/h2&gt;

&lt;p&gt;The main choice on the site is the photo. You do not have to write a prompt, select a model, or piece together scenes. The homepage shows the reference and a cat example before you reach checkout.&lt;/p&gt;

&lt;p&gt;That fixed format comes with a limit: you can change the driver, but you cannot pick another car, outfit, or storyline. I would rather make that clear before someone pays.&lt;/p&gt;

&lt;h2&gt;
  
  
  From upload to download
&lt;/h2&gt;

&lt;p&gt;The flow is photo, delivery email, video pack, payment, then a private order page. Generation starts after payment is confirmed. You can leave the browser and come back through the link in your email to check progress or download the result.&lt;/p&gt;

&lt;p&gt;There is no account to create. A single video costs $9.99 USD, with three-video and ten-video packs available as one-time purchases. The output is about 13 seconds, vertical 9:16 at 720p, with sound and no watermark.&lt;/p&gt;

&lt;p&gt;If generation is confirmed to have failed, the used credit returns to the order for another attempt. Finished videos stay available for 30 days, so you should download a copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part you should check in a sample
&lt;/h2&gt;

&lt;p&gt;A generated clip will not reproduce the reference frame for frame. Faces, fur, clothing, and movement can change. A clear, well-lit portrait gives the model more to work with, but you should still watch the whole result with sound before sharing it. Only upload photos you own or have permission to use.&lt;/p&gt;

&lt;p&gt;The site is live: &lt;a href="https://day1ofowningaroadster.com/" rel="noopener noreferrer"&gt;day1ofowningaroadster.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I would be interested in feedback on the upload and order flow. Is it clear what you will get before you reach payment?&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>Ozil Goal AI: A Photo-to-Video Football Side Project</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Sat, 17 Oct 2026 11:57:50 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/ozil-goal-ai-a-photo-to-video-football-side-project-2nh9</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/ozil-goal-ai-a-photo-to-video-football-side-project-2nh9</guid>
      <description>&lt;p&gt;I built &lt;a href="https://ozil-goal.org/" rel="noopener noreferrer"&gt;Ozil Goal AI&lt;/a&gt;, a small AI football video generator that turns one portrait into a goal video inspired by Özil’s finish against Ludogorets.&lt;/p&gt;

&lt;p&gt;The idea is fairly simple: you supply the person, and the site supplies the football scene. There’s no prompt box to learn and no account to create.&lt;/p&gt;

&lt;h2&gt;
  
  
  A deliberately short input flow
&lt;/h2&gt;

&lt;p&gt;You upload a clear photo of one person, then choose one of four football kits or keep the clothes in the photo. After entering a delivery email and choosing a one-time pack, payment starts the first video.&lt;/p&gt;

&lt;p&gt;The output is a 15-second, 720p, vertical 9:16 MP4. A private order link arrives by email, so you can leave the page while the video processes and return later.&lt;/p&gt;

&lt;p&gt;For a tool built around a single scene, I wanted the choices to stay concrete. A kit selector says more about the possible result than asking someone to describe a football outfit in a prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The website around the model
&lt;/h2&gt;

&lt;p&gt;The web app uses TanStack Start, React, and TypeScript on Cloudflare Workers, with D1 and R2. Video generation comes from an existing model API. My work is the product around it: the input flow, kit choices, payment, order status, and delivery.&lt;/p&gt;

&lt;p&gt;The homepage has playable examples because the output needs to be judged in motion. A convincing face in one frame doesn’t tell you whether it will hold up through a turn. The feet and ball can drift too.&lt;/p&gt;

&lt;p&gt;This is photo-to-video generation, so it creates new frames rather than replacing a face in the original footage. Likeness, clothing, and movement can vary. I try to make that visible before someone pays.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the samples
&lt;/h2&gt;

&lt;p&gt;The samples are free to watch. Personal video generation starts at $12.99 USD as a one-time purchase, with no subscription. Applicable taxes may be added.&lt;/p&gt;

&lt;p&gt;You can find the site at &lt;strong&gt;&lt;a href="https://ozil-goal.org/" rel="noopener noreferrer"&gt;https://ozil-goal.org/&lt;/a&gt;&lt;/strong&gt;. It’s an independent entertainment project with no affiliation to Özil or any club.&lt;/p&gt;

&lt;p&gt;If you build image-to-video tools, I’d be interested in how you show the limits of the output before checkout. For this project, playable examples felt like the most useful place to start.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>Crazy Frog AI Video Generator: Handling Timeouts Without Creating Duplicate Jobs</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Sat, 17 Oct 2026 08:58:11 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/crazy-frog-ai-video-generator-handling-timeouts-without-creating-duplicate-jobs-26km</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/crazy-frog-ai-video-generator-handling-timeouts-without-creating-duplicate-jobs-26km</guid>
      <description>&lt;p&gt;A video generation request can time out after the provider has accepted it. If you treat that timeout as permission to submit again, you can end up paying for two generations while your user waits for one.&lt;/p&gt;

&lt;p&gt;That's a failure case I wanted to account for while building &lt;a href="https://crazy-frog-ai.org/" rel="noopener noreferrer"&gt;Crazy Frog AI Video Generator&lt;/a&gt;. The interface takes two photos, assigns a rider and a chaser, and creates an invisible motorcycle chase. Behind it, I use TypeScript on Cloudflare Workers, D1 for job state, and R2 for the finished media.&lt;/p&gt;

&lt;p&gt;The part I want to share is how I separate a browser retry, a provider status check, and a new paid task. They need different rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the browser request attached to one job
&lt;/h2&gt;

&lt;p&gt;The client supplies a request ID. On the server, I validate the inputs and use that ID as the job ID. I store the validated inputs and the provider request body before contacting the video service.&lt;/p&gt;

&lt;p&gt;When the browser repeats a request with the same ID, the server checks the owner and stored inputs. Matching requests return the existing job. Reusing the ID with different inputs produces an error.&lt;/p&gt;

&lt;p&gt;This handles repeated HTTP requests on our side. The remote submission still needs its own recovery path: our database and the provider don't share a transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record submission intent before the network call
&lt;/h2&gt;

&lt;p&gt;Before creating a provider task, the worker writes a submission timestamp to the job row. It stores the provider's task ID when the response arrives.&lt;/p&gt;

&lt;p&gt;A later worker can make a decision from those two fields:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Submission marker&lt;/th&gt;
&lt;th&gt;Provider task ID&lt;/th&gt;
&lt;th&gt;Next action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Absent&lt;/td&gt;
&lt;td&gt;Absent&lt;/td&gt;
&lt;td&gt;Claim the job and attempt the first submission&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Present&lt;/td&gt;
&lt;td&gt;Present&lt;/td&gt;
&lt;td&gt;Check that existing task&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Present&lt;/td&gt;
&lt;td&gt;Absent&lt;/td&gt;
&lt;td&gt;Pause for reconciliation; don't submit another task&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The third row includes an uncomfortable case. A worker might save the marker and stop before sending the request. We then pause a job that the provider never received.&lt;/p&gt;

&lt;p&gt;I accept that tradeoff in the current implementation. Without a verified way to deduplicate a provider submission, I can't distinguish that case from a provider accepting the request and losing the response. For an ambiguous submission, the app keeps the video allowance reserved and shows a support-review message. A confirmed rejection follows a separate failure path that restores the allowance.&lt;/p&gt;

&lt;p&gt;If I add a provider-supported idempotency key later, I'll need to verify its scope and retention window before changing this behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  A lease needs protection against late writes
&lt;/h2&gt;

&lt;p&gt;A worker claims a pending job with a two-minute lease. Another worker can take over after that lease expires.&lt;/p&gt;

&lt;p&gt;That creates a second problem: the first worker may still be waiting on a provider or storage operation. When it resumes, it must not overwrite the newer worker's state.&lt;/p&gt;

&lt;p&gt;I use an incrementing claim token, stored in the attempts column. Claiming a job increments it. State updates check both that token and the lease. This is the guard used by the update helper, with the SET clause omitted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;lease_until&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'queued'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'processing'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'composing'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'saving'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also include the claim token in output object names. If an older worker finishes an upload late, it writes to a separate object rather than replacing the newer output.&lt;/p&gt;

&lt;p&gt;These checks protect local state and files. They don't make an external API call exactly-once, so the submission marker still matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recover each stage on its own
&lt;/h2&gt;

&lt;p&gt;The workflow has separate stages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;queued -&amp;gt; processing -&amp;gt; composing -&amp;gt; saving -&amp;gt; succeeded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The video provider generates silent output. A separate composition task adds the selected scene's audio. The app then downloads and validates the result and stores it in R2 before marking the job as succeeded.&lt;/p&gt;

&lt;p&gt;If storage fails after composition, recovery can continue from the existing task and result. Starting video generation over would repeat the most expensive part of the workflow.&lt;/p&gt;

&lt;p&gt;Audio composition also has a submission marker and task ID. Moving a paid operation into a later stage doesn't remove the lost-response problem.&lt;/p&gt;

&lt;p&gt;For callbacks, I verify the request, identify the stored job, and wake processing. The worker asks the provider for current task state rather than accepting the callback's output as final. A reconciliation pass checks pending jobs too, with backoff and jitter between attempts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd check before changing the retry policy
&lt;/h2&gt;

&lt;p&gt;The cases I care about include a repeated client request, a timeout after provider acceptance, a worker resuming after its lease expires, and an R2 write failing after composition. Each case needs a different recovery action.&lt;/p&gt;

&lt;p&gt;I haven't removed the manual work from ambiguous submissions. For now, I prefer that visible operational limit to creating a second paid task because the first response went missing.&lt;/p&gt;

&lt;p&gt;You can see the product this supports at &lt;a href="https://crazy-frog-ai.org/" rel="noopener noreferrer"&gt;crazy-frog-ai.org&lt;/a&gt;. If you've handled ambiguous submissions with a provider that offers limited task lookup, I'd be interested in how you reconcile them.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Building Unforgettable Car AI: one portrait, one car reveal</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Thu, 15 Oct 2026 07:59:28 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/building-unforgettable-car-ai-one-portrait-one-car-reveal-460m</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/building-unforgettable-car-ai-one-portrait-one-car-reveal-460m</guid>
      <description>&lt;p&gt;I built &lt;a href="https://unforgettablecar.org/" rel="noopener noreferrer"&gt;Unforgettable Car AI&lt;/a&gt;, a small web app that turns a portrait into a car reveal video.&lt;/p&gt;

&lt;p&gt;You may have seen the format: someone holds up a 2 and a 4 card, covers the camera with a hand, and reveals a car when the lens clears. The app follows that sequence, with your photo as the starting point.&lt;/p&gt;

&lt;p&gt;The idea is easy to explain in a sentence. Making the creation flow equally easy to understand is the part I care about.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small set of choices
&lt;/h2&gt;

&lt;p&gt;The homepage has examples you can watch before signing in. There are short previews and full 15-second clips, so you can see the movement and the reveal before deciding whether to make one.&lt;/p&gt;

&lt;p&gt;To create a video, sign in with Google, upload a clear portrait, and choose a car and a setting. A regular selfie works; you do not need to be holding cards or standing beside a vehicle.&lt;/p&gt;

&lt;p&gt;There are three cars: a black SUV, a green supercar, and a red coupe. You can pair any of them with an underground garage, a rooftop parking area, or a city street. That gives you nine combinations without a long configuration form.&lt;/p&gt;

&lt;p&gt;There is no prompt to write. The reference sequence guides the card gesture, the hand over the lens, and the reveal. When the job finishes, the result appears in My videos, where you can watch and download it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What sits around the video model
&lt;/h2&gt;

&lt;p&gt;The web app uses TanStack Start, React, and Cloudflare Workers. It connects to an existing video generation service.&lt;/p&gt;

&lt;p&gt;The application work is in the flow around that service: collecting a portrait, keeping the car and scene choices separate, tracking the generation job, and making the finished file available to the person who requested it.&lt;/p&gt;

&lt;p&gt;For this kind of app, those steps are a big part of the experience. Uploading a photo is quick. Generation takes longer. The interface needs to make it clear that the request is still being processed and where the result will appear.&lt;/p&gt;

&lt;p&gt;The examples matter for a similar reason. A screenshot cannot show whether a hand movement or a transition feels right. Watching a full sample gives someone a more useful basis for deciding whether to try it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The output and its limits
&lt;/h2&gt;

&lt;p&gt;The result is a 15-second, 16:9 MP4, with a 720p output setting. It is silent: the Unforgettable song is not bundled with the download. Adding music or making a vertical edit is a separate step.&lt;/p&gt;

&lt;p&gt;The model creates new footage, so facial details, hands, clothing, and timing can vary. I would watch the entire clip before sharing it, especially the moment when the hand crosses the face.&lt;/p&gt;

&lt;p&gt;Viewing the public examples is free. Personal video generation is paid through one-time packs, with a single-video pack currently at $12.99 USD before applicable taxes. There is no subscription, and a generation confirmed as failed returns its credit.&lt;/p&gt;

&lt;p&gt;Uploads and generated videos are private to your account. Photos are retained for up to seven days and videos for 30 days, so you should download anything you want to keep.&lt;/p&gt;

&lt;p&gt;You can see the samples at &lt;a href="https://unforgettablecar.org/" rel="noopener noreferrer"&gt;unforgettablecar.org&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you take a look, I would be interested in feedback on the creation flow. Do the examples make the output clear enough, and is there anything you would want to know before uploading a photo?&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>I built a two-photo video generator for the Alors on danse keyboard meme</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Tue, 13 Oct 2026 16:36:57 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-a-two-photo-video-generator-for-the-alors-on-danse-keyboard-meme-1f18</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-a-two-photo-video-generator-for-the-alors-on-danse-keyboard-meme-1f18</guid>
      <description>&lt;p&gt;The first photo becomes the keyboard player. The second becomes the partner reacting alongside them. You can use two people, two pets, or one of each.&lt;/p&gt;

&lt;p&gt;That is the starting point for &lt;a href="https://alorsondanseaivideo.net/" rel="noopener noreferrer"&gt;Alors on Danse AI&lt;/a&gt;, a small website I built around the Stromae and Jamel Debbouze white-studio keyboard sketch. It makes one specific scene: a 16-second video with your chosen duo and the original audio.&lt;/p&gt;

&lt;p&gt;I wanted the setup to be understandable before anyone uploaded a photo. The page has two slots, each labeled with a role. You do not need to write a prompt, find a template file, or install an editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  One scene, two choices
&lt;/h2&gt;

&lt;p&gt;The left/right distinction is a small UI detail that matters. “Upload two images” leaves a question unanswered: which subject does what?&lt;/p&gt;

&lt;p&gt;The first slot is the keyboard player on the left. The second is the expressive partner on the right. Both roles are replaced. The studio, movement reference, duration, and soundtrack stay fixed.&lt;/p&gt;

&lt;p&gt;That also sets a clear boundary for the product. It will not make a different dance, change the music, or turn into a general video editor. The useful choice here is who you put into the scene.&lt;/p&gt;

&lt;h2&gt;
  
  
  A dog on the keyboard
&lt;/h2&gt;

&lt;p&gt;One public example uses a synthetic golden retriever photo for the keyboard player and a synthetic human portrait for the partner.&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%2Fxu5kzt9ynp3d7dhi4acn.jpg" 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%2Fxu5kzt9ynp3d7dhi4acn.jpg" alt="Still from the generated dog-and-human example" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is a still from the 16-second test example. That sample was generated at 480p; the standard paid output is 720p.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://alorsondanseaivideo.net/examples" rel="noopener noreferrer"&gt;examples page&lt;/a&gt; shows the two input photos beside the generated clip. It also includes the scene reference, labeled separately. I think seeing the inputs and result together is more useful than a polished clip with no explanation of where it came from.&lt;/p&gt;

&lt;p&gt;Pets are the fun part, but also a good reminder of the limits. The scene asks them to perform upright, human-like gestures. Fur, paws, faces, and movement can change during generation. A recognizable subject in one frame does not guarantee an equally convincing result throughout the clip.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens behind the button
&lt;/h2&gt;

&lt;p&gt;The app uses React and TanStack Start on Cloudflare Workers. D1 stores accounts, credits, and job state; R2 stores uploaded photos and finished videos.&lt;/p&gt;

&lt;p&gt;The generation pipeline has two stages. Seedance 2.5 through KIE generates the video with audio generation turned off. A separate FAL step merges the fixed soundtrack into the result.&lt;/p&gt;

&lt;p&gt;Separating those steps means the job is not finished just because the video model returned a file. The audio merge and storage still need to complete before there is an MP4 to download.&lt;/p&gt;

&lt;p&gt;There is also the less exciting part of a paid generation app: repeated clicks, delayed callbacks, and uncertain provider responses. The backend tracks each request, checks provider results, and has a recovery job for tasks that need another look. A confirmed failed generation restores one video credit. An uncertain result stays under review rather than immediately triggering another paid generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it, and knowing what to expect
&lt;/h2&gt;

&lt;p&gt;The user flow is short: sign in with Google, choose a video pack, upload one photo for each role, and generate. Progress and completed downloads appear in My videos.&lt;/p&gt;

&lt;p&gt;The standard output is a 16-second, 720p, 16:9 landscape MP4. Packs are one-time purchases, starting at $12.99 for one video, without a subscription. There is no free generation trial; the public examples are available to watch before buying.&lt;/p&gt;

&lt;p&gt;Clear, separate photos work best. Group shots and obscured faces make the role assignment harder. Use photos you have permission to upload.&lt;/p&gt;

&lt;p&gt;Photos are retained for up to seven days and finished videos for 30 days, so download the MP4 if you want to keep it. The restored credit after a failed task is a generation credit, not a cash refund.&lt;/p&gt;

&lt;p&gt;You can see the current version at &lt;a href="https://alorsondanseaivideo.net/" rel="noopener noreferrer"&gt;alorsondanseaivideo.net&lt;/a&gt;. I would be interested in feedback on the two-role setup: is it obvious which photo goes where, and do the examples give you enough information to judge the result before paying?&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>Why I Kept My AI Video Tool to One Scene</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Sun, 11 Oct 2026 12:27:00 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/why-i-kept-my-ai-video-tool-to-one-scene-3095</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/why-i-kept-my-ai-video-tool-to-one-scene-3095</guid>
      <description>&lt;p&gt;I built &lt;a href="https://bulin47.online/" rel="noopener noreferrer"&gt;Bulin 47 AI&lt;/a&gt;, a small browser tool that takes one portrait and makes a version of the Bulin 47 freestyle clip.&lt;/p&gt;

&lt;p&gt;The result is a 10-second vertical video. The portrait guides the main rapper’s appearance, while a fixed reference video guides the performance and surrounding scene. The reference soundtrack is included.&lt;/p&gt;

&lt;p&gt;The main product decision was to keep it to that one scene.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deciding what the user actually needs to choose
&lt;/h2&gt;

&lt;p&gt;A general video generator needs room for prompts, model selection, reference footage, duration and output settings. For this particular clip, most of those choices can be made ahead of time.&lt;/p&gt;

&lt;p&gt;I’ve already prepared the reference clip, prompt and audio. The person using the site mainly needs to decide who should appear in the video.&lt;/p&gt;

&lt;p&gt;That makes the upload form smaller, but it also sets a clear boundary. The tool doesn’t offer custom choreography or a library of different performances. If you want a different scene, this isn’t the right tool for it.&lt;/p&gt;

&lt;p&gt;The photo and video reference have separate jobs. A clear portrait helps with appearance; the reference supplies the movement. There’s no need to record yourself performing or upload a full-body shot.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small form still needs to explain the result
&lt;/h2&gt;

&lt;p&gt;The flow is straightforward: watch the reference, sign in with Google, upload a portrait and submit a generation. You can follow its progress in &lt;strong&gt;My videos&lt;/strong&gt;, then download the finished MP4.&lt;/p&gt;

&lt;p&gt;The output is fixed at 720p, 10 seconds and a 9:16 aspect ratio. Those details belong near the upload form, because they affect whether the result will work for someone’s intended use.&lt;/p&gt;

&lt;p&gt;There are less tidy details to explain too. A face can look convincing in the first frame and change when the person turns. Hands and timing need checking. The model creates new frames, so appearance and motion can vary between generations.&lt;/p&gt;

&lt;p&gt;A completed job doesn’t necessarily mean the person will like the result. That matters when explaining credits: confirmed failed generations return the credit, while a completed video with an unsatisfying likeness is a different situation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the examples honest
&lt;/h2&gt;

&lt;p&gt;The homepage includes the reference and trend examples so people can see the scene before paying. Those gallery clips come from outside creators, are credited on the site and weren’t generated by this service.&lt;/p&gt;

&lt;p&gt;That distinction needs to be visible. A reference clip shows what the tool is aiming for; it doesn’t prove what every generation will look like.&lt;/p&gt;

&lt;p&gt;Making a video requires a paid credit. There are no free generations at the moment. Credits are sold in one-time packs, with no subscription or automatic renewal. The current options are on the &lt;a href="https://bulin47.online/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few limits worth stating upfront
&lt;/h2&gt;

&lt;p&gt;Photos are kept for up to seven days and finished videos for 30 days. Download any result you want to keep. Get permission before using someone else’s portrait, and label shared clips as AI-generated.&lt;/p&gt;

&lt;p&gt;Keeping the scope small gives me a specific workflow to improve: one portrait, one scene, one downloadable result. I’m interested in whether that constraint makes the process easier to understand, and where the form still leaves people guessing.&lt;/p&gt;

&lt;p&gt;You can see it at &lt;a href="https://bulin47.online/" rel="noopener noreferrer"&gt;bulin47.online&lt;/a&gt;. If you take a look, I’d appreciate feedback on the upload flow and how clearly it explains what you’ll get.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>showdev</category>
    </item>
    <item>
      <title>AI Raindance: make a 15-second music video from one or two portraits</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Sun, 11 Oct 2026 04:21:33 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/ai-raindance-make-a-15-second-music-video-from-one-or-two-portraits-33o2</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/ai-raindance-make-a-15-second-music-video-from-one-or-two-portraits-33o2</guid>
      <description>&lt;p&gt;I built &lt;a href="https://airaindance.online/" rel="noopener noreferrer"&gt;AI Raindance&lt;/a&gt;, a web app that puts people from your photos into a short video inspired by Dave and Tems' Raindance music video.&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%2Fgx8s2ihl9hvqmy1jslsl.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%2Fgx8s2ihl9hvqmy1jslsl.png" alt="AI Raindance: 15 seconds, solo or duo, with sound" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You choose who appears in the scene. The app supplies the reference, the waterfront setting and the original audio. There's no prompt to write: upload one or two portraits, choose the roles, and generate a 15-second MP4.&lt;/p&gt;

&lt;p&gt;Here's how it works, and what to check before spending a video credit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the cast
&lt;/h2&gt;

&lt;p&gt;There are two modes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Photos&lt;/th&gt;
&lt;th&gt;Who appears&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Solo&lt;/td&gt;
&lt;td&gt;One portrait&lt;/td&gt;
&lt;td&gt;The person in your photo becomes the singer; the companion is removed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duo&lt;/td&gt;
&lt;td&gt;Two separate portraits&lt;/td&gt;
&lt;td&gt;Photo 1 is the companion, and photo 2 is the singer.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For Duo, you don't need a picture of both people together. Use a separate portrait for each person. If you've put the singer in the first slot, use &lt;strong&gt;Swap roles&lt;/strong&gt; before generating.&lt;/p&gt;

&lt;p&gt;Both modes cost one video credit. Adding a second person doesn't cost extra.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose a photo where the face is easy to see
&lt;/h2&gt;

&lt;p&gt;A front-facing portrait or a slight angle is a good starting point. Keep the face large enough to see clearly, with even lighting and no sunglasses or heavy filters. Group shots and faces far from the camera are harder to work with.&lt;/p&gt;

&lt;p&gt;The app accepts JPEG, PNG and WebP files, up to 10 MB per photo. Use your own photos, or photos you have permission to use with the subject's consent.&lt;/p&gt;

&lt;p&gt;Sign in before selecting them. The uploaded photos aren't saved as an unfinished draft, so leaving the page before submitting can lose your selection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make and download the video
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Sign in with Google. If your balance is empty, buy a one-time video pack.&lt;/li&gt;
&lt;li&gt;Choose Solo or Duo, add your portraits and check the role labels.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Generate&lt;/strong&gt; to submit the task and use one credit.&lt;/li&gt;
&lt;li&gt;Open &lt;strong&gt;My videos&lt;/strong&gt; to follow its progress, play the result and download the MP4.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once you've submitted the task, you can close the page. Return with the same Google account to find it again. You don't need to keep a browser tab open throughout generation.&lt;/p&gt;

&lt;p&gt;The finished video is &lt;strong&gt;15 seconds, 768P, in a 4:3 frame&lt;/strong&gt;, with the original reference audio included. Your portrait changes the person on screen; it doesn't turn your voice into the song.&lt;/p&gt;

&lt;p&gt;Watch the whole clip before sharing it. Facial details, expressions and singing timing can vary between AI results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Look at the sample before buying
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://airaindance.online/examples" rel="noopener noreferrer"&gt;examples page&lt;/a&gt; separates the original reference footage from a generated Solo sample.&lt;/p&gt;

&lt;p&gt;That sample is a short 4.5-second clip made from a fictional AI portrait, with its model-generated soundtrack. It isn't a full preview of a purchased video. Purchased videos use the fixed 15-second template and original Raindance audio.&lt;/p&gt;

&lt;p&gt;The template is deliberately specific: you can choose the portraits and roles, but you can't change the song, scene or duration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost and file storage
&lt;/h2&gt;

&lt;p&gt;Generation is paid. The current packs are $9.99 for one video, $24.99 for three, or $69.99 for ten. There is no subscription, and purchased credits don't expire. If a task is confirmed as failed, its credit is returned automatically.&lt;/p&gt;

&lt;p&gt;Photos are kept for seven days and finished videos for thirty days. Download the MP4 while it's available. Uploads and finished videos stay in your account; they aren't automatically added to the public examples page.&lt;/p&gt;

&lt;p&gt;AI Raindance is an independent project, with no affiliation with Dave or Tems.&lt;/p&gt;

&lt;p&gt;You can try the flow at &lt;a href="https://airaindance.online/" rel="noopener noreferrer"&gt;airaindance.online&lt;/a&gt;. I'd welcome feedback on the Solo/Duo controls, especially whether the companion and singer roles are clear before you upload.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built a one-photo Burning Bridges video generator</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Sun, 11 Oct 2026 03:59:51 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-a-one-photo-burning-bridges-video-generator-1ja1</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-a-one-photo-burning-bridges-video-generator-1ja1</guid>
      <description>&lt;p&gt;I built &lt;a href="https://drakeburningbridgesai.video/" rel="noopener noreferrer"&gt;Drake Burning Bridges AI&lt;/a&gt;, a small web app that takes one portrait and generates a personal version of the Burning Bridges party scene.&lt;/p&gt;

&lt;p&gt;You choose the main character with your photo. I’ve configured the video reference and prompt, so you don’t have to write scene instructions. The output is a 20-second, 720p, landscape MP4 with the supplied soundtrack.&lt;/p&gt;

&lt;p&gt;For this post, I wanted to share the implementation choices behind that short upload flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the input small
&lt;/h2&gt;

&lt;p&gt;The studio accepts one portrait. The photo provides the appearance reference for the main subject; the video reference guides the party setting, gestures and camera movement.&lt;/p&gt;

&lt;p&gt;I kept the scene and output settings fixed. Users can choose who appears in the foreground, but they can’t cast each guest or change the duration. That limits what you can make, and it gives the upload page a clear job: help you choose a usable photo and understand the expected result.&lt;/p&gt;

&lt;p&gt;The server checks file size, image type and dimensions. A file extension or a browser-provided MIME type alone isn’t enough to validate an upload.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;I’m using React and TanStack Start for the app, with Better Auth for authentication. The application runs on Cloudflare Workers. I store task and credit records in D1, and uploaded photos and finished videos in R2.&lt;/p&gt;

&lt;p&gt;The app calls an external video generation service. Workers handle the application flow rather than running video inference themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treating generation as a saved job
&lt;/h2&gt;

&lt;p&gt;A video request needs to survive the user leaving the page. I store the job before processing it and track its progress through these states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;queued → processing → composing → saving → succeeded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The composition step adds the supplied soundtrack. I mark a task as successful after saving the finished video, rather than when the generation provider first reports success.&lt;/p&gt;

&lt;p&gt;Users can return to My videos to check an existing task. The browser doesn’t have to stay open for the whole operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling retries and credits
&lt;/h2&gt;

&lt;p&gt;Each generation uses one credit. I use a request ID to connect a submission to its job. If the same user sends the same request again, the application returns the existing job. Reusing that ID with different input is rejected.&lt;/p&gt;

&lt;p&gt;In the database, I connect job creation to the credit debit with a trigger. If the debit can’t complete, the job insert rolls back. I also give failed-job credit returns a unique ledger reference, so processing the same failure again doesn’t add another credit.&lt;/p&gt;

&lt;p&gt;The less obvious case is a timeout while submitting to the video provider. The provider might have accepted the job even though my application didn’t receive its response. Submitting a fresh request at that point could create a second paid generation.&lt;/p&gt;

&lt;p&gt;I record that submission attempt and keep an uncertain outcome for reconciliation. A confirmed failure returns the credit; a missing response needs more investigation. This leaves some cases requiring support review, but avoids treating an unknown result as permission to submit again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting expectations for the video
&lt;/h2&gt;

&lt;p&gt;AI can change facial details or hands as the person moves. A completed job means the user received a video, not that the model preserved their appearance in every frame.&lt;/p&gt;

&lt;p&gt;I’ve included &lt;a href="https://drakeburningbridgesai.video/examples" rel="noopener noreferrer"&gt;examples&lt;/a&gt; so visitors can inspect the intended scene. Those are supplied 480p samples, not evidence of a paid generation through the app. The configured output is 720p.&lt;/p&gt;

&lt;p&gt;The site is an independent project with no affiliation with Drake. Users should upload photos they have permission to use, with the subject’s consent.&lt;/p&gt;

&lt;p&gt;You can see the project at &lt;a href="https://drakeburningbridgesai.video/" rel="noopener noreferrer"&gt;drakeburningbridgesai.video&lt;/a&gt;. I’d welcome feedback on the upload flow, or how you handle uncertain provider responses in apps that charge per task.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built AI Zombie: two photos, a 15-second zombie love story</title>
      <dc:creator>visiohex</dc:creator>
      <pubDate>Sun, 11 Oct 2026 03:55:02 +0000</pubDate>
      <link>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-ai-zombie-two-photos-a-15-second-zombie-love-story-29pc</link>
      <guid>https://dev.to/jy_wang_2c4d36f4d66adea80/i-built-ai-zombie-two-photos-a-15-second-zombie-love-story-29pc</guid>
      <description>&lt;p&gt;I built &lt;a href="https://aizombie.online" rel="noopener noreferrer"&gt;AI Zombie&lt;/a&gt;, a web app that turns two photos into a 15-second zombie love story with sound.&lt;/p&gt;

&lt;p&gt;The story starts with a survivor facing someone they care about who has become a zombie. Instead of shooting, the survivor lowers the weapon and opens their arms. The zombie approaches, they embrace, and the scene cuts to a warm memory of them together.&lt;/p&gt;

&lt;p&gt;You can use photos of two people, or make your dog or cat the zombie and keep yourself as the human survivor.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Upload a photo of the person or pet who becomes the zombie.&lt;/li&gt;
&lt;li&gt;Upload a photo of the human survivor.&lt;/li&gt;
&lt;li&gt;Choose a cabin, city, or chapel setting, and a portrait or landscape format.&lt;/li&gt;
&lt;li&gt;Start the generation, then watch and download the finished video from My videos.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The output is a 15-second, 720p MP4 with audio. The homepage includes example videos if you want to see the style before creating one. Generation uses paid video credits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Giving the model a specific story
&lt;/h2&gt;

&lt;p&gt;I kept the generator focused on one short narrative. You choose the characters and setting; the app supplies the sequence of shots.&lt;/p&gt;

&lt;p&gt;In the implementation, I build the video prompt around timed story beats: recognition, the survivor's decision, the approach, the embrace, and the memory. The transition is the part I care about most. I ask the model to keep the characters in the same positions across the cut, so the embrace connects the present to the memory.&lt;/p&gt;

&lt;p&gt;The pet version needs different instructions. I ask for a natural four-legged trot and a short jump that fits the animal's size. The prompt also specifies that the model should preserve the pet's markings and species rather than give it human limbs or gestures.&lt;/p&gt;

&lt;p&gt;These are instructions to the model, rather than guarantees. Faces, motion, and sound can vary between generations, and the model may miss a detail in the intended sequence.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few implementation details
&lt;/h2&gt;

&lt;p&gt;I built the app with TanStack Start and React, running on Cloudflare Workers. I use D1 for generation records and R2 for media storage. The video generation itself runs through an external API.&lt;/p&gt;

&lt;p&gt;A generation takes longer than a normal page request, so I store it as a job. The UI shows its status while the backend handles submission and completion. Users can return to their video history to find the result.&lt;/p&gt;

&lt;p&gt;I also use a request ID to handle repeated submissions. If a user retries the same request, the backend returns the existing job instead of creating another one. That matters when each generation consumes a paid credit.&lt;/p&gt;

&lt;h2&gt;
  
  
  I'd like your feedback
&lt;/h2&gt;

&lt;p&gt;You can see the examples and try the generator at &lt;a href="https://aizombie.online" rel="noopener noreferrer"&gt;aizombie.online&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I'm interested in whether the story makes sense when you watch it, and whether the upload flow makes the two character roles clear. If you're building an image-to-video app, I'd also like to hear how you handle continuity between shots.&lt;/p&gt;

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