<?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>Forem: Tyler</title>
    <description>The latest articles on Forem by Tyler (@tylerilunga).</description>
    <link>https://forem.com/tylerilunga</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3760641%2Fb8cfe094-b4b3-4f57-8c2a-59ab8d9cf96d.jpg</url>
      <title>Forem: Tyler</title>
      <link>https://forem.com/tylerilunga</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/tylerilunga"/>
    <language>en</language>
    <item>
      <title>How I Built an AI Product Photography Pipeline with 30+ Models (Next.js + Express + Replicate/FAL)</title>
      <dc:creator>Tyler</dc:creator>
      <pubDate>Sun, 08 Feb 2026 21:11:51 +0000</pubDate>
      <link>https://forem.com/tylerilunga/how-i-built-an-ai-product-photography-pipeline-with-30-models-nextjs-express-replicatefal-bp8</link>
      <guid>https://forem.com/tylerilunga/how-i-built-an-ai-product-photography-pipeline-with-30-models-nextjs-express-replicatefal-bp8</guid>
      <description>&lt;p&gt;I've been building &lt;a href="https://pixelmotion.io" rel="noopener noreferrer"&gt;PixelMotion&lt;/a&gt; — a SaaS that takes product photos and transforms them into enhanced images and AI-generated videos. Here's a deep dive into the technical architecture and the lessons I learned orchestrating 30+ AI models in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Ecommerce brands need high-quality product visuals but professional photography is expensive ($500-2000 per product). AI models like Flux, Stable Diffusion, and video generators like Kling, Sora, and Veo can now produce stunning results — but each model has different strengths, APIs, pricing, and failure modes.&lt;/p&gt;

&lt;p&gt;I wanted to build a system where a user uploads a single product photo, and the platform handles everything: background removal, enhancement, upscaling, and even generating marketing videos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;The stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt;: Next.js 15 (App Router) + TailwindCSS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: Express.js + TypeScript + PostgreSQL (Sequelize ORM)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Providers&lt;/strong&gt;: Replicate, FAL AI, OpenAI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt;: Google Cloud Storage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments&lt;/strong&gt;: Stripe (usage-based credits)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Upload → Website Scraper → AI Analysis → Model Selection → Enhancement/Generation → Storage → Delivery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Technical Decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Multi-Provider Model Orchestration
&lt;/h3&gt;

&lt;p&gt;The biggest challenge was abstracting away the differences between AI providers. Replicate uses a prediction-based API with webhooks. FAL uses queue-based async processing. OpenAI has its own patterns.&lt;/p&gt;

&lt;p&gt;I built a unified &lt;code&gt;llmService&lt;/code&gt; that normalizes the interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified model orchestration&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="nx"&gt;llmService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;provider&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;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 'replicate' | 'fal' | 'openai'&lt;/span&gt;
  &lt;span class="na"&gt;model&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;id&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="nx"&gt;normalizedParams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;webhook&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;callbackUrl&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;Each provider adapter handles its own retry logic, timeout behavior, and error mapping. This means adding a new model is just a config change — no service code modifications.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Fallback Chains
&lt;/h3&gt;

&lt;p&gt;AI models fail. A lot. Rate limits, cold starts, model updates that change output quality. I implemented fallback chains:&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;ENHANCEMENT_CHAIN&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;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flux-pro-v2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;replicate&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;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flux-pro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fal&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;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stable-diffusion-xl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;replicate&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;If the primary model fails or times out, the system automatically tries the next one. Users don't see errors — they just get results.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Credit-Based Pricing
&lt;/h3&gt;

&lt;p&gt;Different models have wildly different costs. Sora costs ~10x more than Kling per generation. Instead of flat subscriptions, I went with a credit system where each model costs a different number of credits:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Credits&lt;/th&gt;
&lt;th&gt;Actual Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Flux 2 Pro (photo)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;~$0.05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kling 1.6 (video)&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;~$0.13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sora 2 (video)&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;~$0.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Veo 3.1 (video)&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;~$0.65&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This lets users choose based on their budget and quality needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Automated Product Intelligence
&lt;/h3&gt;

&lt;p&gt;When a user connects their ecommerce store, the platform scrapes product data and uses GPT-4o to analyze:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product category and type&lt;/li&gt;
&lt;li&gt;Target audience&lt;/li&gt;
&lt;li&gt;Brand aesthetic&lt;/li&gt;
&lt;li&gt;Optimal AI models for that product type&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This analysis feeds into prompt generation, so a luxury watch gets different treatment than a kitchen gadget.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Async Job Queue with Polling
&lt;/h3&gt;

&lt;p&gt;AI generation takes 30-120 seconds. I use a job queue pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User submits request → job created with &lt;code&gt;pending&lt;/code&gt; status&lt;/li&gt;
&lt;li&gt;Backend dispatches to AI provider&lt;/li&gt;
&lt;li&gt;Frontend polls every 3 seconds via custom &lt;code&gt;usePolling&lt;/code&gt; hook&lt;/li&gt;
&lt;li&gt;Webhook or poll catches completion → status updated to &lt;code&gt;completed&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Frontend displays result
&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="c1"&gt;// Frontend polling hook (simplified)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useGenerationStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobId&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&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;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/jobs/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="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="s1"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;3000&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&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="nx"&gt;jobId&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;status&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;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Don't trust AI model output blindly.&lt;/strong&gt; Some models occasionally return blank images, corrupted files, or completely wrong outputs. Always validate outputs before serving to users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Cost management is critical.&lt;/strong&gt; Early on, I burned through $200 in a weekend because I didn't have per-user rate limits. Now every generation checks credit balance first, and I have daily cost alerts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Prompt engineering &amp;gt; model selection.&lt;/strong&gt; A well-crafted prompt on a cheaper model often beats a bad prompt on an expensive one. I spent weeks refining prompts and built a versioned prompt system to track changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Webhooks are unreliable.&lt;/strong&gt; Both Replicate and FAL occasionally fail to deliver webhooks. Always implement polling as a fallback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Users don't care about the model.&lt;/strong&gt; They care about the result. Auto-selecting the best model based on the product type improved satisfaction more than letting users choose manually.&lt;/p&gt;

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

&lt;p&gt;Currently working on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multi-photo video generation (combine multiple product angles into one video)&lt;/li&gt;
&lt;li&gt;UGC-style video generation with AI avatars&lt;/li&gt;
&lt;li&gt;Direct publishing to TikTok/YouTube from the platform&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building with AI APIs and want to chat about multi-provider orchestration, fallback patterns, or credit systems — drop a comment. Happy to go deeper on any of these topics.&lt;/p&gt;

&lt;p&gt;You can check out the live product at &lt;a href="https://pixelmotion.io" rel="noopener noreferrer"&gt;pixelmotion.io&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Next.js 15, Express.js, PostgreSQL, Replicate, FAL AI, and OpenAI.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
