<?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: Image Splitting Field Notes</title>
    <description>The latest articles on DEV Community by Image Splitting Field Notes (@quinn-yang).</description>
    <link>https://dev.to/quinn-yang</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%2F4034934%2Fa7dd22a8-c954-4520-8e93-c2df2625a9b9.jpg</url>
      <title>DEV Community: Image Splitting Field Notes</title>
      <link>https://dev.to/quinn-yang</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/quinn-yang"/>
    <language>en</language>
    <item>
      <title>Building a Multilingual Game Guide in Astro Without Duplicate-Content Pages</title>
      <dc:creator>Image Splitting Field Notes</dc:creator>
      <pubDate>Sat, 08 Aug 2026 17:31:21 +0000</pubDate>
      <link>https://dev.to/quinn-yang/building-a-multilingual-game-guide-in-astro-without-duplicate-content-pages-4d79</link>
      <guid>https://dev.to/quinn-yang/building-a-multilingual-game-guide-in-astro-without-duplicate-content-pages-4d79</guid>
      <description>&lt;p&gt;Multilingual SEO pages are easy to generate and surprisingly easy to get wrong.&lt;/p&gt;

&lt;p&gt;If every locale is a literal translation of one template, readers get awkward copy and search engines get a thin cluster of near-duplicates. If every language becomes a completely separate page, editorial updates drift and technical SEO becomes hard to audit.&lt;/p&gt;

&lt;p&gt;I ran into that trade-off while building an Astro field guide for a newly released game. One guide query — the location and use of a specific key — needed an English page plus localized versions for Japanese, Spanish, French, German, and Traditional Chinese.&lt;/p&gt;

&lt;p&gt;Here is the implementation pattern I used.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Keep page structure in Astro and editorial content in data
&lt;/h2&gt;

&lt;p&gt;The page component owns the stable structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;breadcrumb&lt;/li&gt;
&lt;li&gt;answer summary&lt;/li&gt;
&lt;li&gt;quick facts&lt;/li&gt;
&lt;li&gt;route steps&lt;/li&gt;
&lt;li&gt;FAQ&lt;/li&gt;
&lt;li&gt;sources&lt;/li&gt;
&lt;li&gt;related guides&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Localized content lives in a typed JSON collection. Each entry carries its own locale, route, title, description, headings, steps, FAQ answers, and source note.&lt;/p&gt;

&lt;p&gt;The dynamic route stays small:&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;function&lt;/span&gt; &lt;span class="nf"&gt;getStaticPaths&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;guideData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;guides&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;guide&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="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;guide&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&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="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;guide&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;guide&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Astro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives editors one predictable content contract while allowing each locale to use natural terminology instead of inheriting English sentence structure.&lt;/p&gt;

&lt;p&gt;It also makes omissions visible. If a localized page is missing a route step, source note, or FAQ answer, that is a data problem rather than a hidden template branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Build one complete hreflang cluster
&lt;/h2&gt;

&lt;p&gt;The English page lives at the canonical query URL. Localized versions use stable locale folders.&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;alternates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hrefLang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en-US&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/blue-key-location/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;allGuides&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&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="na"&gt;hrefLang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&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 shared layout turns those entries into absolute alternate links using the production site URL. It also emits an &lt;code&gt;x-default&lt;/code&gt; URL that points to the English guide.&lt;/p&gt;

&lt;p&gt;Every page in the cluster references the same set of alternates. This matters more than simply placing a language switcher in the visible navigation: the HTML metadata needs to describe the relationship consistently in both directions.&lt;/p&gt;

&lt;p&gt;Each page also receives its own self-referencing canonical. Localized pages never canonicalize back to English, because they are intended to stand as distinct language results.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Generate structured data from the same content
&lt;/h2&gt;

&lt;p&gt;The article data already contains the information needed for several useful schema types, so I generate JSON-LD from the same source instead of maintaining a second SEO-only copy.&lt;/p&gt;

&lt;p&gt;The page emits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Article&lt;/code&gt; for the guide itself&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BreadcrumbList&lt;/code&gt; for navigation context&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FAQPage&lt;/code&gt; from the visible questions and answers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HowTo&lt;/code&gt; only when the page contains an ordered route&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The conditional HowTo block is important. Adding process schema to every article would make the markup less accurate.&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;routeSection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;guide&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;section&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;section&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&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;howTo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;routeSection&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@context&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="s2"&gt;https://schema.org&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="s2"&gt;@type&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="s2"&gt;HowTo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;routeSection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;routeSection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&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="s2"&gt;HowToStep&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;index&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;step&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;Because the visible guide and JSON-LD share one data source, a route correction updates both.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Localize search intent, not just sentences
&lt;/h2&gt;

&lt;p&gt;The most useful change was editorial rather than technical.&lt;/p&gt;

&lt;p&gt;A guide page is not merely a translation target. Players in different languages may use different names for the same landmark, key item, platform feature, or region. A literal translation can be grammatically correct while still missing the phrase players actually search.&lt;/p&gt;

&lt;p&gt;Each locale therefore owns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;its native title and meta description&lt;/li&gt;
&lt;li&gt;its preferred landmark terminology&lt;/li&gt;
&lt;li&gt;short answer wording&lt;/li&gt;
&lt;li&gt;UI labels&lt;/li&gt;
&lt;li&gt;source and update notes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The underlying route remains evidence-aligned across languages, but the phrasing is written for the reader in that locale.&lt;/p&gt;

&lt;p&gt;I also keep dated evidence labels visible. Game information changes after release, so “verified on” is more honest and maintainable than presenting every fact as permanent.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Make production URLs part of the build
&lt;/h2&gt;

&lt;p&gt;The site is static, but canonical URLs, hreflang links, sitemap entries, and JSON-LD still need the real production origin.&lt;/p&gt;

&lt;p&gt;The build uses a single environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;SITE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://beastofreincarnation.co npm run build
npm run check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That removes hard-coded development origins and makes the generated output portable across static hosts.&lt;/p&gt;

&lt;p&gt;After building, I check the rendered HTML rather than trusting the component source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;canonical is absolute and self-referencing&lt;/li&gt;
&lt;li&gt;every alternate URL returns 200&lt;/li&gt;
&lt;li&gt;the cluster is reciprocal&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;x-default&lt;/code&gt; resolves&lt;/li&gt;
&lt;li&gt;JSON-LD parses&lt;/li&gt;
&lt;li&gt;the page is not accidentally &lt;code&gt;noindex&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Live example
&lt;/h2&gt;

&lt;p&gt;The finished query cluster is visible in the &lt;a href="https://beastofreincarnation.co/blue-key-location/" rel="noopener noreferrer"&gt;Beast of Reincarnation Blue Key location guide&lt;/a&gt;. The English page links to five localized versions, and each version is generated from the same Astro page shape while retaining locale-specific copy.&lt;/p&gt;

&lt;p&gt;The pattern is intentionally small: one component, one content contract, one alternate cluster, and one build-time origin. That has been easier to review than either duplicating page files or building a translation layer inside the component.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would reuse on another project
&lt;/h2&gt;

&lt;p&gt;For another multilingual content site, I would keep the same boundaries:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Put layout and schema generation in components.&lt;/li&gt;
&lt;li&gt;Put claims, steps, labels, and sources in typed content.&lt;/li&gt;
&lt;li&gt;Give every locale a real canonical URL.&lt;/li&gt;
&lt;li&gt;Generate a complete reciprocal hreflang cluster.&lt;/li&gt;
&lt;li&gt;Localize the query language, not only the prose.&lt;/li&gt;
&lt;li&gt;Validate rendered production HTML after every content expansion.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The goal is not to manufacture more URLs. It is to make each localized URL useful enough to deserve being indexed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclosure
&lt;/h2&gt;

&lt;p&gt;I used AI assistance to organize and edit this article. I verified the implementation details against the live Astro project and its rendered production output before publishing.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>webdev</category>
      <category>seo</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Designing a Cost-Aware AI Video Workflow: Show the Price Before Render</title>
      <dc:creator>Image Splitting Field Notes</dc:creator>
      <pubDate>Sat, 01 Aug 2026 06:52:20 +0000</pubDate>
      <link>https://dev.to/quinn-yang/designing-a-cost-aware-ai-video-workflow-show-the-price-before-render-5cm9</link>
      <guid>https://dev.to/quinn-yang/designing-a-cost-aware-ai-video-workflow-show-the-price-before-render-5cm9</guid>
      <description>&lt;p&gt;AI video forms often make the expensive decision feel like a cheap one. A creator chooses a model, duration, resolution, and audio mode, but the interface hides the consequence until after they press &lt;strong&gt;Generate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is more than a pricing problem. It is a product-state problem: the cost is derived from the same controls that define the output, so it should update in the same interaction loop.&lt;/p&gt;

&lt;p&gt;This post explains the pattern I used while building an image-to-video workflow around Grok Imagine 1.5 Preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Make cost a derived field, not a checkout surprise
&lt;/h2&gt;

&lt;p&gt;For the current Grok workflow, the supported duration is 1–15 seconds. The UI offers 480p and 720p. In this implementation, 480p is charged at 1.6 credits per second and 720p at 3 credits per second, rounded up to a whole credit.&lt;/p&gt;

&lt;p&gt;The estimator can stay intentionally small:&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;Resolution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;480p&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="s2"&gt;720p&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;estimateCredits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;duration&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="nx"&gt;resolution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Resolution&lt;/span&gt;&lt;span class="p"&gt;,&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;const&lt;/span&gt; &lt;span class="nx"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;resolution&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;720p&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&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;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;rate&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 important part is not the formula. It is where the formula runs. Duration and resolution are already controlled form state, so the estimate belongs beside the Generate button and should change immediately when either control changes.&lt;/p&gt;

&lt;p&gt;Do not calculate it only on the server and reveal it after submission. The server should still recalculate the authoritative amount, but the client estimate is what lets the user make a decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Model capabilities should shape the form
&lt;/h2&gt;

&lt;p&gt;A generic video form tends to expose every control for every model. That creates impossible combinations and forces error handling to do work the interface could have prevented.&lt;/p&gt;

&lt;p&gt;Instead, describe each model as data:&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;grokImagine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;modes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image-to-video&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;durations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="p"&gt;},&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;index&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;index&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="na"&gt;resolutions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;480p&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="s2"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;native&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&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="s2"&gt;480p&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The form then renders only valid choices. Because this model creates video and audio together, there is no separate audio toggle. Hiding a control is better than showing a switch that does nothing.&lt;/p&gt;

&lt;p&gt;This also reduces drift between marketing copy, the form, and the API. The same capability object can power validation, defaults, and explanatory text.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Design the default around a first successful result
&lt;/h2&gt;

&lt;p&gt;A default is a recommendation. It should not silently consume most of a new user's allowance.&lt;/p&gt;

&lt;p&gt;In this example, an 8-second 480p render costs 13 credits. A new account receives 20 credits, so the default can produce one full result with some room left. Moving the same duration to 720p raises the estimate to 24 credits, and the interface makes that trade-off visible before submission.&lt;/p&gt;

&lt;p&gt;This is a useful acceptance test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can a new user complete the default workflow with the initial allowance?&lt;/li&gt;
&lt;li&gt;Does changing resolution visibly change the estimate?&lt;/li&gt;
&lt;li&gt;Does an unaffordable combination become obvious before the API call?&lt;/li&gt;
&lt;li&gt;Is the default still long enough to demonstrate the product?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Free credits are not useful if they cannot produce a first result.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Put guidance next to the expensive choice
&lt;/h2&gt;

&lt;p&gt;The creator should not need a separate pricing page to understand a control. Short, local guidance works better:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start at 480p while testing motion and framing.&lt;/li&gt;
&lt;li&gt;Move to 720p after the prompt is stable.&lt;/li&gt;
&lt;li&gt;Keep the first clip short when exploring a new visual direction.&lt;/li&gt;
&lt;li&gt;Describe subject motion, camera movement, and environmental motion separately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This turns cost visibility into workflow advice instead of a warning banner.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Revalidate on the server
&lt;/h2&gt;

&lt;p&gt;The preview is advisory. The API must normalize and validate the same settings before creating a provider task.&lt;/p&gt;

&lt;p&gt;A safe request path is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Parse duration as an integer.&lt;/li&gt;
&lt;li&gt;Confirm the selected model supports that duration.&lt;/li&gt;
&lt;li&gt;Confirm the resolution is in the model's capability list.&lt;/li&gt;
&lt;li&gt;Recompute the cost on the server.&lt;/li&gt;
&lt;li&gt;Check the current balance.&lt;/li&gt;
&lt;li&gt;Deduct credits and create the asynchronous task.&lt;/li&gt;
&lt;li&gt;Refund the deduction if the generation fails.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Client and server can share types and configuration, but the server remains authoritative.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Test decisions, not just controls
&lt;/h2&gt;

&lt;p&gt;A useful UI test does more than confirm that a dropdown opens. It verifies the decision loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The 8-second 480p default shows 13 credits.&lt;/li&gt;
&lt;li&gt;Switching to 720p shows 24 credits.&lt;/li&gt;
&lt;li&gt;A 1-second 480p clip rounds up to 2 credits.&lt;/li&gt;
&lt;li&gt;Unsupported durations never appear.&lt;/li&gt;
&lt;li&gt;The submitted payload matches the estimate the user approved.&lt;/li&gt;
&lt;li&gt;A failed generation restores the deducted balance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These checks protect the promise the interface makes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Live reference
&lt;/h2&gt;

&lt;p&gt;The finished interaction is available in the &lt;a href="https://mict.io/grok-video-generator" rel="noopener noreferrer"&gt;MICT Grok image-to-video workflow&lt;/a&gt;. I am the builder of MICT; I am linking it as the concrete implementation discussed above, not as an independent recommendation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclosure
&lt;/h2&gt;

&lt;p&gt;I used AI assistance to edit the structure and wording of this article. I verified the technical details and code examples against the implementation before publishing.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>ux</category>
      <category>ai</category>
    </item>
    <item>
      <title>Designing a Reliable Async AI Video Pipeline in TypeScript</title>
      <dc:creator>Image Splitting Field Notes</dc:creator>
      <pubDate>Fri, 31 Jul 2026 03:46:32 +0000</pubDate>
      <link>https://dev.to/quinn-yang/designing-a-reliable-async-ai-video-pipeline-in-typescript-3kl0</link>
      <guid>https://dev.to/quinn-yang/designing-a-reliable-async-ai-video-pipeline-in-typescript-3kl0</guid>
      <description>&lt;p&gt;Calling an AI video API looks simple in a demo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Send a prompt.&lt;/li&gt;
&lt;li&gt;Wait for a response.&lt;/li&gt;
&lt;li&gt;Show the video.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In production, the model rarely returns a video in the original HTTP request. It creates a remote job, gives you an external task ID, moves through provider-specific states, and eventually returns either a media URL or an error. Your application has to keep user state, billing state, moderation state, and provider state consistent while that happens.&lt;/p&gt;

&lt;p&gt;This article presents a provider-neutral reference architecture for products such as &lt;a href="https://mict.io/" rel="noopener noreferrer"&gt;MICT&lt;/a&gt;, an AI image and short-video workspace. The code samples are illustrative rather than a verbatim description of MICT's production implementation. The useful part is not a particular API call; it is the set of boundaries around that call.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real workflow is a state machine
&lt;/h2&gt;

&lt;p&gt;A reliable generation request has more stages than “loading” and “done”:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request
  -&amp;gt; authenticate
  -&amp;gt; validate model settings
  -&amp;gt; moderate input
  -&amp;gt; calculate cost
  -&amp;gt; create and persist local task
  -&amp;gt; reserve or deduct credits
  -&amp;gt; create provider task
  -&amp;gt; attach external task ID
  -&amp;gt; poll provider
  -&amp;gt; normalize provider state
  -&amp;gt; moderate output
  -&amp;gt; complete or fail
  -&amp;gt; refund when required
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Treating this as an explicit state machine gives every layer a shared vocabulary. A compact internal set is usually enough:&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;GenerationStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;running&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="s2"&gt;processing&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="s2"&gt;completed&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="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Providers can keep their own vocabulary at the edge:&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;function&lt;/span&gt; &lt;span class="nf"&gt;mapProviderState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&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;GenerationStatus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;waiting&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;queuing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;running&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;generating&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;processing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fail&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;running&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important design choice is that the rest of the product never branches on every provider's spelling. The adapter does that once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep local and external task IDs separate
&lt;/h2&gt;

&lt;p&gt;Create and persist your own task before calling the provider.&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;taskId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createLocalTaskId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;persistGeneration&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;running&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;externalTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;providerModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;attachExternalTaskId&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;externalTaskId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;externalTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The local ID belongs to your product. Use it for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authorization checks;&lt;/li&gt;
&lt;li&gt;generation history;&lt;/li&gt;
&lt;li&gt;credit transactions;&lt;/li&gt;
&lt;li&gt;support references;&lt;/li&gt;
&lt;li&gt;analytics;&lt;/li&gt;
&lt;li&gt;idempotency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The external ID belongs to the provider. Use it only when querying or canceling the remote job.&lt;/p&gt;

&lt;p&gt;This separation matters when providers change, when one provider is temporarily replaced, or when a support ticket refers to a local job that never received an external task ID.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate settings before money moves
&lt;/h2&gt;

&lt;p&gt;Different video models accept different combinations of mode, duration, resolution, aspect ratio, output count, and audio settings. Do not let the client invent those combinations.&lt;/p&gt;

&lt;p&gt;Keep capabilities in a server-side model configuration:&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;ModelConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;modes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;t2v&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="s2"&gt;i2v&lt;/span&gt;&lt;span class="dl"&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="nl"&gt;durations&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;resolutions&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;aspectRatios&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="nf"&gt;calculateCredits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;duration&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;resolution&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;outputCount&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="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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reject unsupported values before moderation, billing, or provider submission:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;modes&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;mode&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RequestError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unsupported generation mode&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolutions&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;resolution&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RequestError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unsupported resolution&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Client-side validation is useful for feedback. Server-side validation is the boundary that protects your bill and your data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put moderation before generation
&lt;/h2&gt;

&lt;p&gt;Input moderation should happen before the provider task is created and before credits are charged.&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;moderation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;moderateRequest&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;moderation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;ok&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="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;moderation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;creditsCharged&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="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;This avoids paying the upstream provider for a request your own policy would reject. It also lets the UI distinguish a policy decision from a technical failure.&lt;/p&gt;

&lt;p&gt;Output moderation is a separate gate. A provider can successfully generate media that your application should not release. The output should not become &lt;code&gt;completed&lt;/code&gt; until that gate passes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;provider success
  -&amp;gt; extract result URL
  -&amp;gt; moderate result
  -&amp;gt; completed

provider success
  -&amp;gt; extract result URL
  -&amp;gt; output blocked
  -&amp;gt; failed with a policy-safe message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not collapse input moderation, provider policy errors, and output moderation into one generic “generation failed” toast. They have different retry rules and support paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make credit operations idempotent
&lt;/h2&gt;

&lt;p&gt;The most damaging bugs in an AI generation product are often financial state bugs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the provider rejected the request but credits stayed deducted;&lt;/li&gt;
&lt;li&gt;polling ran twice and refunded twice;&lt;/li&gt;
&lt;li&gt;a database write failed after the provider task started;&lt;/li&gt;
&lt;li&gt;the browser retried a request after a network timeout.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every charge should have a stable source key:&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;await&lt;/span&gt; &lt;span class="nf"&gt;decreaseCredits&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;creditCost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sourceType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;generation_charge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sourceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;taskId&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;A refund should reference that same source:&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;await&lt;/span&gt; &lt;span class="nf"&gt;refundCreditsBySource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;sourceType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;generation_charge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sourceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;transactionType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refund&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database should enforce uniqueness for the relevant source or transaction key. Application-level &lt;code&gt;if (!refunded)&lt;/code&gt; checks are helpful, but they are not enough under concurrent polling.&lt;/p&gt;

&lt;p&gt;You need at least two refund paths:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The provider task could not be created.&lt;/li&gt;
&lt;li&gt;The provider accepted the task, but it later reached a terminal failure.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&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;external&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;attachExternalTaskId&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;externalTaskId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;external&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;refundByGenerationSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&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;For terminal failure during polling:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;refundByGenerationSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;markGenerationFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;publicErrorMessage&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;When refunds are idempotent, repeated polling becomes much less frightening.&lt;/p&gt;

&lt;h2&gt;
  
  
  Polling should be boring
&lt;/h2&gt;

&lt;p&gt;WebSockets can improve perceived responsiveness, but polling is often the most robust first implementation for provider jobs that take seconds or minutes.&lt;/p&gt;

&lt;p&gt;The status endpoint should:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;authenticate the request;&lt;/li&gt;
&lt;li&gt;load the local generation;&lt;/li&gt;
&lt;li&gt;verify that it belongs to the user;&lt;/li&gt;
&lt;li&gt;return immediately for local terminal states;&lt;/li&gt;
&lt;li&gt;query the provider only for non-terminal jobs;&lt;/li&gt;
&lt;li&gt;normalize the result;&lt;/li&gt;
&lt;li&gt;update local state;&lt;/li&gt;
&lt;li&gt;return a small, stable response.
&lt;/li&gt;
&lt;/ol&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;StatusResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GenerationStatus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;progress&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;resultUrl&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;errorCode&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;errorMessage&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client does not need the provider's raw payload. Raw payloads leak implementation details and make frontend behavior depend on unstable third-party schemas.&lt;/p&gt;

&lt;p&gt;A reasonable browser loop looks like this:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;waitForGeneration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskId&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="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;while &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;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errorMessage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&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;Production code should also stop polling when the component unmounts, pause or slow down in a hidden tab, and apply a maximum client-side wait. The job can continue on the server even if the browser stops asking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parse provider results defensively
&lt;/h2&gt;

&lt;p&gt;Even one provider may return different result shapes across models:&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;"resultUrls"&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="s2"&gt;"https://..."&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;/code&gt;&lt;/pre&gt;

&lt;/div&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;"video_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://..."&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;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;"output"&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;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://..."&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep result extraction inside the adapter and accept only a non-empty string:&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;function&lt;/span&gt; &lt;span class="nf"&gt;extractResultUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&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="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&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;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resultUrls&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urls&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&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;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;videoUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;video_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&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;A provider saying &lt;code&gt;success&lt;/code&gt; without a usable result URL is not a completed user experience. Keep the job non-terminal or move it to a clearly diagnosed failure path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not let a late success overwrite a failure
&lt;/h2&gt;

&lt;p&gt;Asynchronous systems can produce awkward orderings:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;a job appears successful;&lt;/li&gt;
&lt;li&gt;output moderation starts;&lt;/li&gt;
&lt;li&gt;another process marks the generation failed;&lt;/li&gt;
&lt;li&gt;a late update tries to mark it completed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Use a conditional update:&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;update&lt;/span&gt; &lt;span class="n"&gt;generations&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'completed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'failed'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the update affects zero rows, reload the current state and return that state. Terminal safety decisions should win over late success events.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve error categories
&lt;/h2&gt;

&lt;p&gt;Users need different actions for different errors:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Error category&lt;/th&gt;
&lt;th&gt;User action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;insufficient credits&lt;/td&gt;
&lt;td&gt;add credits or choose cheaper settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;provider unavailable&lt;/td&gt;
&lt;td&gt;retry later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;input policy blocked&lt;/td&gt;
&lt;td&gt;revise the request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;provider policy blocked&lt;/td&gt;
&lt;td&gt;revise the request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;output policy blocked&lt;/td&gt;
&lt;td&gt;contact support only if the decision seems wrong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unknown technical failure&lt;/td&gt;
&lt;td&gt;retry or contact support with the task ID&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Return stable error codes and separately maintain a user-safe message:&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RequestError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;We could not start this generation right now.&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="s2"&gt;provider_unavailable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;creditsCharged&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not expose upstream account balances, internal model routes, or raw policy messages in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to test
&lt;/h2&gt;

&lt;p&gt;A happy-path video is not enough. Test the state transitions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invalid model settings do not charge credits;&lt;/li&gt;
&lt;li&gt;input moderation rejection does not charge credits;&lt;/li&gt;
&lt;li&gt;provider creation failure refunds once;&lt;/li&gt;
&lt;li&gt;provider terminal failure refunds once;&lt;/li&gt;
&lt;li&gt;repeated status polling does not duplicate refunds;&lt;/li&gt;
&lt;li&gt;one user cannot read another user's task;&lt;/li&gt;
&lt;li&gt;provider success without a URL does not become completed;&lt;/li&gt;
&lt;li&gt;output moderation failure cannot be overwritten by a late success;&lt;/li&gt;
&lt;li&gt;terminal local states do not keep querying the provider;&lt;/li&gt;
&lt;li&gt;unknown provider states stay recoverable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best tests target invariants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;one accepted generation &amp;lt;= one charge
one failed charged generation &amp;lt;= one refund
completed =&amp;gt; usable result URL
failed-by-policy =&amp;gt; result URL is not released
task owner =&amp;gt; only user allowed to read status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The larger lesson
&lt;/h2&gt;

&lt;p&gt;An AI video interface is a distributed system with a creative UI attached to it. The model call is only one step. The product becomes trustworthy when every other step—validation, authorization, billing, polling, moderation, and failure recovery—has an explicit boundary.&lt;/p&gt;

&lt;p&gt;Start with a small internal state machine. Keep provider vocabulary at the edge. Give every charge a stable source. Make refunds idempotent. Treat moderation as part of the lifecycle, not a preflight checkbox. Then make polling intentionally boring.&lt;/p&gt;

&lt;p&gt;That foundation is less glamorous than a generation demo, but it is what lets the demo become a product.&lt;/p&gt;




&lt;p&gt;Disclosure: This article was prepared for MICT, which is referenced once as the product context for this architecture. AI tools assisted with drafting and editing; the code, claims, and final text were reviewed before publication.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>ai</category>
      <category>architecture</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Pixel Math Behind Seamless Instagram Carousels</title>
      <dc:creator>Image Splitting Field Notes</dc:creator>
      <pubDate>Sat, 18 Jul 2026 08:08:14 +0000</pubDate>
      <link>https://dev.to/quinn-yang/the-pixel-math-behind-seamless-instagram-carousels-533l</link>
      <guid>https://dev.to/quinn-yang/the-pixel-math-behind-seamless-instagram-carousels-533l</guid>
      <description>&lt;p&gt;A seamless carousel is a geometry problem before it is an export problem.&lt;/p&gt;

&lt;p&gt;If one portrait slide is &lt;strong&gt;1080 × 1350 px&lt;/strong&gt;, the master canvas must be an exact multiple of 1080 pixels wide:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;master_width = slide_width × slide_count
master_height = slide_height
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Examples:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Slides&lt;/th&gt;
&lt;th&gt;Master canvas&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3240 × 1350&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5400 × 1350&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;7560 × 1350&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why seams break
&lt;/h2&gt;

&lt;p&gt;The most common failures happen before slicing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The master image was resized to a width that does not divide into whole pixels.&lt;/li&gt;
&lt;li&gt;Text, faces, or logos sit directly on a cut boundary.&lt;/li&gt;
&lt;li&gt;Individual tiles are resized again after export.&lt;/li&gt;
&lt;li&gt;Filenames do not preserve the posting order.&lt;/li&gt;
&lt;li&gt;Different compression settings are applied to different tiles.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A reliable export workflow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Choose the final slide ratio.&lt;/li&gt;
&lt;li&gt;Multiply the slide width by the number of slides.&lt;/li&gt;
&lt;li&gt;Add guides at every slide boundary.&lt;/li&gt;
&lt;li&gt;Finish the composition on the single master canvas.&lt;/li&gt;
&lt;li&gt;Export the master once.&lt;/li&gt;
&lt;li&gt;Split it into equal horizontal sections.&lt;/li&gt;
&lt;li&gt;Test the numbered pieces in an Instagram draft on the actual phone.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For private client artwork, the safest splitter is one that does not upload the source image. I built &lt;a href="https://www.image-splitter.com/" rel="noopener noreferrer"&gt;Image Splitter Online&lt;/a&gt; around that constraint: the image is processed locally in the browser, cut lines are previewed before export, and PNG, JPG, WebP, or ZIP output is available without an account.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small implementation detail
&lt;/h2&gt;

&lt;p&gt;If you are building your own splitter, distribute remainder pixels deterministically instead of rounding every tile independently. For a width &lt;code&gt;W&lt;/code&gt; and &lt;code&gt;N&lt;/code&gt; columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base&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;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;W&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;N&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;remainder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;W&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;N&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;widths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;N&lt;/span&gt; &lt;span class="p"&gt;},&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;base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;remainder&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="mi"&gt;0&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;That guarantees the exported widths sum back to the source width. It also avoids the one-pixel gaps or overlaps that appear when every cut boundary is rounded separately.&lt;/p&gt;

&lt;p&gt;The final check is visual: reconstruct the row from the exported files and verify it matches the master exactly before publishing.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>tutorial</category>
      <category>javascript</category>
      <category>privacy</category>
    </item>
  </channel>
</rss>
