<?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: 陳小聰</title>
    <description>The latest articles on DEV Community by 陳小聰 (@_38c7a446220e42aadf00b).</description>
    <link>https://dev.to/_38c7a446220e42aadf00b</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%2F2030006%2Fb86e7716-87c9-4841-a08f-18d9c17f117d.jpg</url>
      <title>DEV Community: 陳小聰</title>
      <link>https://dev.to/_38c7a446220e42aadf00b</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_38c7a446220e42aadf00b"/>
    <language>en</language>
    <item>
      <title>How to Build a Reliable AI Video Generation Pipeline</title>
      <dc:creator>陳小聰</dc:creator>
      <pubDate>Sat, 10 Oct 2026 15:08:06 +0000</pubDate>
      <link>https://dev.to/_38c7a446220e42aadf00b/how-to-build-a-reliable-ai-video-generation-pipeline-8a4</link>
      <guid>https://dev.to/_38c7a446220e42aadf00b/how-to-build-a-reliable-ai-video-generation-pipeline-8a4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I work on Bulin47AI, the project used as a case study below. This draft was prepared with AI assistance.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An AI video generation pipeline has an awkward failure mode: your server sends a request, the connection times out, and you cannot tell whether the provider started the job. Retrying may create a second billable generation. Telling the user it failed may be equally wrong.&lt;/p&gt;

&lt;p&gt;This design problem shaped [&lt;a href="https://bulin47ai.net" rel="noopener noreferrer"&gt;Bulin47AI&lt;/a&gt;](&lt;a href="https://bulin47ai.net/" rel="noopener noreferrer"&gt;https://bulin47ai.net/&lt;/a&gt;), our small studio built around one prepared video scene. A user supplies one portrait and chooses a 5, 12, or 23-second cut. The model attempts to replace the central performer; our application is designed to restore the source audio and deliver a private MP4. The model can still miss the target or change background details, so the result needs review.&lt;/p&gt;

&lt;p&gt;The useful lesson is the job design around that model call. Here is a pattern you can adapt to other asynchronous media APIs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Define a narrow generation contract&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before sending a request, decide exactly what the user approved. For a fixed-source workflow, that means more than a prompt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A versioned source clip and its hash.&lt;/li&gt;
&lt;li&gt;The selected cut, starting point, output resolution, and aspect ratio.&lt;/li&gt;
&lt;li&gt;The portrait file after server-side validation.&lt;/li&gt;
&lt;li&gt;A quote version and the credit amount shown to the user.&lt;/li&gt;
&lt;li&gt;A stable job ID and the account that owns it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our creation route checks the selected source hash and recalculates the quote on the server. If either changed since the user opened the form, it asks for another review instead of silently applying the new settings. A generic prompt-based product may have different fields, but the rule is the same: persist an immutable description of the accepted request.&lt;/p&gt;

&lt;p&gt;ts：&lt;br&gt;
「&lt;br&gt;
// Simplified model, not a copy of the production schema.&lt;br&gt;
type JobStatus =&lt;br&gt;
  | "preparing"&lt;br&gt;
  | "uploading"&lt;br&gt;
  | "submitting"&lt;br&gt;
  | "submission_unknown"&lt;br&gt;
  | "queued"&lt;br&gt;
  | "in_progress"&lt;br&gt;
  | "processing"&lt;br&gt;
  | "completed"&lt;br&gt;
  | "failed";&lt;/p&gt;

&lt;p&gt;type GenerationJob = {&lt;br&gt;
  id: string;&lt;br&gt;
  ownerId: string;&lt;br&gt;
  sourceHash: string;&lt;br&gt;
  sourceVersion: string;&lt;br&gt;
  quoteVersion: string;&lt;br&gt;
  reservedCredits: number;&lt;br&gt;
  status: JobStatus;&lt;br&gt;
  providerRequestId?: string;&lt;br&gt;
};&lt;br&gt;
」&lt;/p&gt;

&lt;p&gt;This record gives the user something concrete to return to if their browser closes or the request stalls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Reserve credits before the billable boundary&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An asynchronous job should have a clear order of operations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validate the account, portrait, source selection, consent, and quote.&lt;/li&gt;
&lt;li&gt;Create the job and reserve its credits atomically.&lt;/li&gt;
&lt;li&gt;Save the inputs in private storage.&lt;/li&gt;
&lt;li&gt;Persist a &lt;code&gt;submitting&lt;/code&gt; state &lt;strong&gt;before&lt;/strong&gt; calling the provider.&lt;/li&gt;
&lt;li&gt;Submit using a stable idempotency key tied to the job ID.&lt;/li&gt;
&lt;li&gt;Save the provider's request ID as soon as it is returned.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fourth step matters. If a worker crashes after the provider receives the request, a later worker must not interpret the job as a fresh request. The saved state tells it that submission may already have happened.&lt;/p&gt;

&lt;p&gt;Do not treat an HTTP timeout as proof of rejection. Only a definitive provider rejection should move directly to a failed state with an automatic credit return. For an ambiguous outcome, hold the job for reconciliation.&lt;/p&gt;

&lt;p&gt;ts「&lt;br&gt;
await saveJob({ ...job, status: "submitting" });&lt;/p&gt;

&lt;p&gt;try {&lt;br&gt;
  const receipt = await provider.submit(input, {&lt;br&gt;
    idempotencyKey: job.id,&lt;br&gt;
  });&lt;br&gt;
  await saveJob({ ...job, status: "queued", providerRequestId: receipt.id });&lt;br&gt;
} catch (error) {&lt;br&gt;
  if (isDefinitiveRejection(error)) {&lt;br&gt;
    await failAndReturnCredits(job);&lt;br&gt;
  } else {&lt;br&gt;
    await saveJob({ ...job, status: "submission_unknown" });&lt;br&gt;
  }&lt;br&gt;
}&lt;br&gt;
」&lt;/p&gt;

&lt;p&gt;This is illustrative code. Its safety depends on the provider's idempotency behavior and on your own atomic credit and job operations. Test those properties rather than assuming a header solves duplicate billing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Give uncertainty its own state&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Many job systems model only &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;completed&lt;/code&gt;, and &lt;code&gt;failed&lt;/code&gt;. That leaves no honest answer when submission is uncertain.&lt;/p&gt;

&lt;p&gt;Our workflow uses &lt;code&gt;submission_unknown&lt;/code&gt; for that case. It does not resubmit during status polling. The user sees that the request needs review, and the reserved credits stay held until the outcome is known. That is less convenient than an automatic retry, but it avoids promising a refund while a chargeable generation may still be running.&lt;/p&gt;

&lt;p&gt;A useful state path is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;preparing -&amp;gt; uploading -&amp;gt; submitting -&amp;gt; queued -&amp;gt; in_progress
                                             -&amp;gt; processing -&amp;gt; completed
                         \-&amp;gt; submission_unknown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a production system, reconciliation should check any provider receipt, webhook, dashboard record, or idempotency lookup available. Record the evidence used to settle the job. If your provider cannot answer whether a timed-out call was accepted, make that limitation visible to operators and users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Treat the returned video as untrusted input&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;“Provider says completed” is not the same as “the user has a playable result.” Downloading, processing, storing, and serving the output are separate steps that can each fail.&lt;/p&gt;

&lt;p&gt;For our fixed scene, we check the generated video's dimensions and duration against the approved source. We allow only small timing drift, then map the generated video stream to the original source audio. After processing, we probe the final MP4 again for expected codecs, size, dimensions, and duration. Only then does the job become &lt;code&gt;completed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The distinction also improves error handling. A model failure and a temporary download or storage failure need different recovery paths. If the model already ran, a storage retry should not start another generation.&lt;/p&gt;

&lt;p&gt;Keep the source portrait and finished file private. A result route should verify the signed-in user owns the job before returning media, including byte-range playback and downloads. A hard-to-guess URL alone is not a substitute for an ownership check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Show the same states to users and operators&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Users need a durable result page that can answer three questions: Did my request start? What is happening now? Were my credits used or returned? Operators need the corresponding job ID, provider request ID, timestamps, and error category.&lt;/p&gt;

&lt;p&gt;Avoid a progress bar that implies precision your provider does not supply. Clear states such as “uploading,” “generation in progress,” “processing,” and “needs review” are more useful than a fabricated percentage. If a user retries, make it a deliberate new job with a new quote, not a hidden second call behind a refresh button.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frequently asked questions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Is an idempotency key enough to prevent duplicate charges?&lt;/p&gt;

&lt;p&gt;Only if the provider documents and honors its behavior for your request type. Persist the local submission state first, retain the key, and test timeout and replay scenarios.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When should credits be returned?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Return them after a definitive failure under your billing rules. An ambiguous submission needs reconciliation before you classify it as failed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why validate the output if the provider reports success?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The returned file may have the wrong duration, framing, audio, or codec, or may fail during download. Validate the final file that the user will actually play.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A reliable AI video generation pipeline is mostly about preserving the truth of a request across slow, billable, and uncertain steps. Start with a versioned input contract, persist the submission boundary, represent unknown outcomes explicitly, and validate the final media before marking a job complete. Those safeguards matter even when the creative workflow is as narrow as a single scene.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>video</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Built a Photo-to-Cross-Stitch Pattern Maker That Runs in Your Browser</title>
      <dc:creator>陳小聰</dc:creator>
      <pubDate>Fri, 07 Aug 2026 06:09:19 +0000</pubDate>
      <link>https://dev.to/_38c7a446220e42aadf00b/i-built-a-photo-to-cross-stitch-pattern-maker-that-runs-in-your-browser-2mko</link>
      <guid>https://dev.to/_38c7a446220e42aadf00b/i-built-a-photo-to-cross-stitch-pattern-maker-that-runs-in-your-browser-2mko</guid>
      <description>&lt;p&gt;&lt;a href="https://stitchfromphoto.com" rel="noopener noreferrer"&gt;Photo-to-cross-stitch&lt;/a&gt; conversion looks like a resizing problem. It is not. A pixelated preview can look convincing and still be frustrating to stitch. It may contain too many colors, lack readable symbols, provide no reliable dimensions, or become useless when printed.&lt;/p&gt;

&lt;p&gt;I built StitchFromPhoto to handle the practical part of that workflow. It turns an image into a counted cross-stitch chart in the browser, lets you tune the result before committing to it, and keeps the source photo on your device.&lt;/p&gt;

&lt;p&gt;The useful output is a pattern, not a pixelated image&lt;/p&gt;

&lt;p&gt;A cross-stitch preview only answers one question. It shows roughly what the finished piece might look like. A usable pattern must also tell you how many stitches wide and tall the design is, which thread color belongs in each square, whether similar colors remain distinguishable on paper, and how large the result will be on your chosen fabric.&lt;/p&gt;

&lt;p&gt;That distinction shaped the app. The color preview is useful, but the symbol chart, thread key, stitch totals, fabric dimensions, and printable pages are the real deliverables.&lt;/p&gt;

&lt;p&gt;What the photo-to-cross-stitch pattern maker does&lt;/p&gt;

&lt;p&gt;The workflow starts with a sample image, so anyone can explore the controls before uploading a file. It also accepts JPG, PNG, and WebP images up to 20 MB.&lt;/p&gt;

&lt;p&gt;The main controls are stitch width, DMC color count, and fabric count. You can choose a pattern from 30 to 120 stitches wide, limit the palette to between 6 and 36 DMC colors, and calculate the finished size for 14, 16, 18, or 22 count Aida.&lt;/p&gt;

&lt;p&gt;You can move between the original photo, a color stitch preview, and a high-contrast symbol view. The thread key lists every retained DMC color code and the number of stitches assigned to it.&lt;/p&gt;

&lt;p&gt;Creating and previewing a pattern is free. High-resolution PNG and print-ready PDF downloads are unlocked per source image. I wanted that boundary to be visible before checkout rather than hidden behind the final button.&lt;/p&gt;

&lt;p&gt;How the browser turns pixels into stitches&lt;/p&gt;

&lt;p&gt;The conversion pipeline uses deterministic image processing rather than generative AI. The same image and settings produce the same chart.&lt;/p&gt;

&lt;p&gt;First, the browser reduces the image to the requested stitch width while preserving its aspect ratio. Each resulting pixel represents one cross stitch. The app then reads those pixels with the Canvas API and sends them to the pattern-building function.&lt;/p&gt;

&lt;p&gt;Every sampled color is matched against a curated DMC palette using a weighted RGB distance calculation. The app counts the initial matches, keeps the most frequently used shades up to the selected limit, and remaps excluded shades to the nearest retained color.&lt;/p&gt;

&lt;p&gt;Finally, each retained color receives a chart symbol and a stitch count. The renderer adds normal grid lines along with heavier guides every ten stitches, making longer rows easier to follow.&lt;/p&gt;

&lt;p&gt;The algorithm is intentionally compact. It does not pretend that automatic conversion replaces craft judgment. A portrait, a logo, and a flower motif require different compromises between detail and effort, which is why those controls remain in the hands of the stitcher.&lt;/p&gt;

&lt;p&gt;Keeping the source photo out of the backend&lt;/p&gt;

&lt;p&gt;Personal pattern projects often begin with a family photo, a pet, or a gift idea. Uploading that image to an image-processing server should not be required for a conversion the browser can already perform.&lt;/p&gt;

&lt;p&gt;StitchFromPhoto opens the selected file through a temporary browser address and reads its pixels with the Canvas API. Pattern generation and export rendering both happen on the device.&lt;/p&gt;

&lt;p&gt;The purchase flow does not receive the photo either. The browser creates a salted SHA-256 identifier for the selected file and sends only that identifier to checkout. It binds export access to the image without including the image bytes in the request.&lt;/p&gt;

&lt;p&gt;This creates a useful architectural boundary. The server handles payment state, while the browser owns the image and generated chart.&lt;/p&gt;

&lt;p&gt;The less glamorous details mattered most&lt;/p&gt;

&lt;p&gt;A symbol view exposes complexity that a smooth color preview can hide. Isolated stitches and nearly identical shades become much more obvious in the black-and-white chart, allowing the stitcher to simplify the design before buying floss.&lt;/p&gt;

&lt;p&gt;Physical size also belongs beside the pixel controls. A setting such as 80 stitches wide is abstract. Showing the finished width on 14-count or 18-count Aida turns it into a project someone can plan.&lt;/p&gt;

&lt;p&gt;Large patterns need tiled output. Shrinking a detailed chart until it fits on one sheet defeats the purpose, so the PDF uses multiple pattern pages and includes the thread key and project dimensions.&lt;/p&gt;

&lt;p&gt;Color matching also needs an honest caveat. Screen colors are approximations of physical thread. Lighting, monitor calibration, fabric color, and dye lots can all affect the result, so important palettes should still be checked with real floss.&lt;/p&gt;

&lt;p&gt;The current converter focuses on full cross stitches. It does not automatically add backstitch, fractional stitches, beads, or the manual cleanup that an experienced designer may apply to a polished commercial pattern.&lt;/p&gt;

&lt;p&gt;Try the pattern maker&lt;/p&gt;

&lt;p&gt;You can test the complete preview workflow with the sample bouquet at &lt;a href="https://stitchfromphoto.com" rel="noopener noreferrer"&gt;https://stitchfromphoto.com&lt;/a&gt;. Change the stitch width, reduce the palette, and switch to the symbol view. That sequence quickly reveals whether the design is becoming simpler or merely losing useful detail.&lt;/p&gt;

&lt;p&gt;If you upload your own picture, look at the silhouette first. Then inspect the face, lettering, or other detail that makes the subject recognizable. A smaller, cleaner chart is often more enjoyable to stitch than a larger chart filled with one-off colors.&lt;/p&gt;

&lt;p&gt;Frequently asked questions&lt;/p&gt;

&lt;p&gt;Is StitchFromPhoto free to use?&lt;/p&gt;

&lt;p&gt;Creating and previewing a pattern is free. Downloading the high-resolution PNG and printable PDF requires a per-image export unlock.&lt;/p&gt;

&lt;p&gt;Is my photo uploaded to a server?&lt;/p&gt;

&lt;p&gt;No. The source image is read and converted in the browser. Checkout receives a private browser-scoped identifier, not the photo itself.&lt;/p&gt;

&lt;p&gt;Are the DMC matches exact?&lt;/p&gt;

&lt;p&gt;They are practical digital approximations. Check important colors with physical floss under the lighting and against the fabric you plan to use.&lt;/p&gt;

&lt;p&gt;Does it generate backstitch or fractional stitches?&lt;/p&gt;

&lt;p&gt;Not currently. The generated chart uses full cross stitches and is best treated as a strong starting point that you can inspect and refine.&lt;/p&gt;

&lt;p&gt;Closing thought&lt;/p&gt;

&lt;p&gt;Building a photo-to-cross-stitch pattern maker turned out to be less about producing a pixel effect and more about translating an image into physical constraints. The grid, palette, symbols, fabric size, privacy boundary, and printable output all have to agree before the result is useful at a craft table.&lt;/p&gt;

&lt;p&gt;Try StitchFromPhoto at &lt;a href="https://stitchfromphoto.com" rel="noopener noreferrer"&gt;https://stitchfromphoto.com&lt;/a&gt; and tell me which part of the conversion workflow you would improve next.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>nextjs</category>
    </item>
  </channel>
</rss>
