<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>MerchSage Engineering Blog</title>
        <link>https://merchsage.com/blog</link>
        <description>AI-driven print-on-demand, Kestra orchestration, and LLM-collaborative engineering.</description>
        <lastBuildDate>Thu, 07 May 2026 18:18:33 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>MerchSage Engineering Blog</title>
            <url>https://merchsage.com/og-image.png</url>
            <link>https://merchsage.com/blog</link>
        </image>
        <copyright>© 2026 PRISMATIKORBIT LDA</copyright>
        <item>
            <title><![CDATA[How we get clean design cutouts from a generative model]]></title>
            <link>https://merchsage.com/blog/clean-cutouts-from-generative-models</link>
            <guid>https://merchsage.com/blog/clean-cutouts-from-generative-models</guid>
            <pubDate>Sun, 10 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Generating against five maximally-contrasting blurred natural scenes, picking the most distant in HSL space, then stripping with Photoroom — and the two approaches that didn't work.]]></description>
            <content:encoded><![CDATA[<p>A print-on-demand pipeline needs <em>transparent</em> artwork. A t-shirt design has to live on white cotton, black cotton, and a hundred shades in between. The artwork itself has to ship as a transparent PNG with a clean alpha channel — no halo, no fringe, no ghost of the background bleeding into the design&#39;s edges.</p>
<p>The hard part is that generative image models don&#39;t produce transparent PNGs. They produce JPEG-grade RGB on a background. Whatever you do to remove the background later has to fight whatever the model decided to put there. After a lot of failed experiments, we landed on a technique that works for ~95% of designs on the first attempt. This post is what it is, and what we tried first.</p>
<h2 id="the-goal">The goal</h2>
<p>The output is a transparent PNG of the artwork, edges crisp, alpha properly anti-aliased at the boundary. The artwork lands on a Printful product, gets composited onto a mockup, and then onto a real garment. Any fringe, smear, or color contamination from the original background is visible in production.</p>
<p>We need this to be reliable, automated, and run on every concept the pipeline generates. There is no human in the loop reviewing edge quality.</p>
<h2 id="what-didnt-work-1-asking-the-model-for-transparency">What didn&#39;t work #1: asking the model for transparency</h2>
<p>Ask for &quot;transparent background&quot; or &quot;PNG with alpha channel&quot; and you get a <em>checkerboard</em> — the gray-and-white pattern design tools use to depict transparency. The model has seen this all over its training data and produces the visual metaphor. Background removers can strip it, but the result is ragged, with gray bleeding into the artwork&#39;s edges.</p>
<h2 id="what-didnt-work-2-flat-solid-backgrounds">What didn&#39;t work #2: flat solid backgrounds</h2>
<p>Next attempt: instruct the model to generate against a single solid color, picked to maximally contrast with the artwork. Use a chroma key remover.</p>
<p>Two failure modes:</p>
<ol>
<li><strong>The model doesn&#39;t actually produce flat solid color.</strong> It produces something <em>close</em>, with subtle texture, gradients, and lighting from the artwork bleeding into the background. Chroma keying treats anything within a tolerance band as &quot;background&quot; and anything outside as &quot;foreground&quot; — but with subtle texture, the tolerance has to be wide, and now you&#39;re keying out parts of the artwork that share a hue.</li>
<li><strong>Color contamination at the boundary.</strong> The model treats the background as part of the image. Light from the artwork reflects onto the background. The boundary pixels are a blend of foreground and background colors. When you key out the background, the boundary pixels get the wrong alpha — and you can <em>see</em> the contaminating color in the cutout.</li>
</ol>
<p>You can paper over this with edge-aware mattes and Photoshop tricks, but at scale it&#39;s brittle.</p>
<h2 id="what-didnt-work-3-distinctive-patterns">What didn&#39;t work #3: distinctive patterns</h2>
<p>If the matter struggles with subtle texture, give it something unmistakable. We tried concentric rings, checkerboards, in black-and-white and in colors picked to contrast with the artwork.</p>
<p>Same failure mode as #2, in some ways worse. A pattern shares too much <em>essence</em> with the artwork — high-frequency, graphical, edge-heavy. Background removers separate something graphic from something not. When the background is itself a deliberate graphic, that distinction collapses, and the matter keys arbitrarily on whichever pattern element happens to be near the boundary. Edges came out worse than with flat color, not better.</p>
<p>The signal here pointed at the answer: the background needed to look fundamentally <em>unlike</em> the artwork — different in style, different in spatial frequency.</p>
<h2 id="the-technique-that-worked">The technique that worked</h2>
<p>Generative models are great at producing rich, naturalistic scenes. They&#39;re bad at producing flat color. So: ask for a rich scene that maximally contrasts with the artwork&#39;s palette, then use a real background remover that handles natural imagery well.</p>
<p>The pipeline:</p>
<ol>
<li><strong>Curate a small palette of high-contrast natural scenes.</strong> Five is enough.</li>
<li><strong>Pick the scene whose representative color is maximally distant from the artwork&#39;s color palette in HSL space.</strong></li>
<li><strong>Generate the artwork over that scene as the background.</strong></li>
<li><strong>Strip the background via <a href="https://photoroom.com">Photoroom</a>.</strong></li>
</ol>
<p>The scenes are deliberately chosen to span the hue wheel. Whatever palette the artwork has, at least one scene will sit far from it.</p>
<h2 id="the-five-scenes">The five scenes</h2>
<p>This is <code>SCENE_PALETTE</code> from <code>packages/python/merchsage/merchsage/concepts/backgrounds.py</code>:</p>
<table>
<thead>
<tr>
<th>Hex</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>#2D5A27</code></td>
<td>Oblique view of a dense coniferous forest patch on a hillside — no sky, no clouds — heavily blurred/defocused with soft, diffused natural daylight</td>
</tr>
<tr>
<td><code>#C8A23D</code></td>
<td>Close-up of a dry wheat field at golden hour — no sky, no horizon — heavily blurred/defocused with warm, diffused amber sunlight filtering through the stalks</td>
</tr>
<tr>
<td><code>#3A6B7C</code></td>
<td>Close-up of smooth river stones submerged in shallow clear water — no sky, no surface reflections — heavily blurred/defocused with cool, diffused overcast daylight</td>
</tr>
<tr>
<td><code>#A0522D</code></td>
<td>Close-up of layered sandstone rock face with natural iron-oxide striations — no sky, no vegetation — heavily blurred/defocused with soft, diffused warm daylight</td>
</tr>
<tr>
<td><code>#7B5EA7</code></td>
<td>Close-up of a dense lavender field in full bloom — no sky, no paths — heavily blurred/defocused with soft, diffused cool daylight casting gentle violet shadows</td>
</tr>
</tbody></table>
<p>A few non-obvious choices:</p>
<ul>
<li><strong>Heavily blurred / defocused.</strong> This is critical. A sharp natural scene gives the background remover too many edges to confuse with the artwork&#39;s edges. A defocused scene reads as a soft color field with low spatial frequency — easy to subtract.</li>
<li><strong>No sky, no horizon, no clouds.</strong> Skies are bright and uniform; they create artificial flat regions where chroma keying behavior re-emerges. The instructions explicitly forbid them.</li>
<li><strong>Diffused light.</strong> Direct sunlight produces hot specular highlights that compete with the artwork. Diffused light gives uniform exposure across the frame.</li>
<li><strong>Hue spread.</strong> Forest green, wheat gold, river blue, sandstone red-brown, lavender violet. Five hues, ~72° apart on the wheel. Whatever the artwork is, at least one scene will be in opposition.</li>
</ul>
<h2 id="greedy-max-min-selection-in-hsl-space">Greedy max-min selection in HSL space</h2>
<p>For a given concept, we want the scene whose representative color is <em>furthest from any color in the artwork palette</em>.</p>
<pre><code class="language-python">def _hsl_distance(hsl1, hsl2):
    &quot;&quot;&quot;Squared distance between two HSL tuples (wrapping hue).&quot;&quot;&quot;
    dh = min(abs(hsl1[0] - hsl2[0]), 1 - abs(hsl1[0] - hsl2[0]))
    ds = hsl1[1] - hsl2[1]
    dl = hsl1[2] - hsl2[2]
    return dh**2 + ds**2 + dl**2
</code></pre>
<p>The hue distance wraps at 1.0 — red and magenta are <em>close</em>, even though their numeric hue values are far apart. The selection is a simple greedy max-min:</p>
<blockquote>
<p>for each scene, compute the <em>minimum</em> HSL distance to any artwork color. pick the scene with the <em>maximum</em> of those minima.</p>
</blockquote>
<p>In other words: pick the scene that is far from the artwork&#39;s <em>closest</em> color, not its average. This is the right objective because the failure mode of background removal is &quot;this background pixel got confused with that artwork pixel&quot; — what matters is the worst pair, not the typical pair.</p>
<p>The HSL space here is preferable to RGB or LAB. Hue captures the perceptual axis humans (and Photoroom&#39;s matting model) actually disambiguate on. Saturation and lightness are secondary signals — they don&#39;t dominate.</p>
<h2 id="stripping-with-photoroom">Stripping with Photoroom</h2>
<p>Once the artwork is generated against the selected scene, <a href="https://photoroom.com">Photoroom</a>&#39;s API does the actual matting. Two reasons we picked it:</p>
<ol>
<li>It produces clean alpha at edges, including hair-like fine detail. Most chroma keyers don&#39;t.</li>
<li>It handles natural imagery well. The scene is full-frame nature; Photoroom doesn&#39;t get confused by it because nature is what it&#39;s trained on.</li>
</ol>
<p>The remaining pipeline is mundane: alpha threshold to clean up sub-1% alpha noise, crop to the artwork&#39;s bounding box, save as PNG. Up to 20 concurrent Photoroom calls per pipeline run.</p>
<h2 id="catching-the-misses">Catching the misses</h2>
<p>Photoroom gets us most of the way, but it isn&#39;t perfect. A scene texture occasionally bleeds into a thin negative-space region. The model sometimes paints the artwork with edges that share a hue with the scene, and the matte cuts in too far. A halo of warm pixels can ring an element after the cutout. We can&#39;t ship those.</p>
<p>The fan rater downstream is the cleanup pass. It gets <em>two</em> images per design:</p>
<ul>
<li><strong>Image A</strong>: the original generation, scene background still present.</li>
<li><strong>Image B</strong>: the artwork after the scene has been stripped.</li>
</ul>
<p>Its job is to compare them and flag structural artifacts in B that came from A&#39;s scene. The vocabulary is specific: a <em>remnant</em> is a blob, halo, smear, or patch — something with shape and location that isn&#39;t part of the intended design. A ring of grass-green pixels around a coffee cup is a remnant. A semi-transparent smudge of sky in empty space is a remnant.</p>
<p>What <em>isn&#39;t</em> a remnant matters as much, because without these exclusions the rater over-flags:</p>
<ul>
<li><strong>Scene-lighting tints baked into design colors.</strong> Generating against a wheat field warms the foreground hues; against river stones it cools them. Those tints persist into the cutout. They look like contamination but aren&#39;t — they&#39;re the model&#39;s rendering, and they&#39;re in every design.</li>
<li><strong>Soft anti-aliased edges.</strong> A 1–2px alpha gradient is how a clean cutout <em>should</em> look. Penalizing it produces crunchy, aliased designs.</li>
<li><strong>Intentional elements the artwork description names</strong> — stars, dots, glow rings. Without this clause the rater flags legitimate stylistic flourishes as scene bleed.</li>
</ul>
<p>Designs the rater flags as having visible remnants get thrown away.</p>
<h2 id="results">Results</h2>
<p>Roughly 95% of designs come out of Photoroom clean on the first pass. The rater catches most of what doesn&#39;t. By the time designs reach production, visible cutout artifacts show up about 1 in 300.</p>
<h2 id="the-takeaway">The takeaway</h2>
<p>The generative model does what it&#39;s good at: producing a rich naturalistic scene. The classical CV pipeline (color-distance scene selection + Photoroom matting) does what <em>it&#39;s</em> good at: separating foreground from a non-degenerate background.</p>
<p>Most of the time, when a generative pipeline gives bad output, the answer isn&#39;t a better prompt or a better model. It&#39;s recognizing which step in the pipeline is asking the model to do something it&#39;s bad at, and replacing that step with a deterministic one.</p>
]]></content:encoded>
            <category>image-generation</category>
            <category>design</category>
            <category>gemini</category>
            <category>photoroom</category>
            <category>color-theory</category>
        </item>
        <item>
            <title><![CDATA[Agentic Kestra: making an LLM a first-class flow author]]></title>
            <link>https://merchsage.com/blog/agentic-kestra</link>
            <guid>https://merchsage.com/blog/agentic-kestra</guid>
            <pubDate>Sat, 09 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[An MCP that closes the author → sync → test → observe → DB-inspect → reset loop, plus the codebase contracts that make LLM-driven flow authoring actually work.]]></description>
            <content:encoded><![CDATA[<p>MerchSage runs on <a href="https://kestra.io">Kestra</a>. The merch pipeline itself is around 30 flows. We liked the architecture enough that we extended it to the rest of the business — ops and marketing run as Kestra flows too — bringing the total close to 60, all backed by a single Postgres database that every stage reads from and writes to. Every line of code in the system was written by Claude — not a single human line. The humans direct product and architecture, with Claude&#39;s help on both.</p>
<p>This post is about what changes when you take that seriously. Specifically: what tools an LLM needs to be a real Kestra collaborator, and what code-level contracts you have to enforce so it can debug its own work.</p>
<h2 id="the-problem">The problem</h2>
<p>If you let an LLM write a Kestra flow with only a filesystem and a shell, here&#39;s what you&#39;ll observe.</p>
<p>It writes the flow. It tries to &quot;test&quot; by reading the YAML back. It cannot dispatch a run. If it could dispatch, it cannot follow logs. If it could follow logs, it cannot inspect the rows that the flow&#39;s Python tasks wrote. If it cannot inspect rows, it cannot debug the data — only the syntax. It will then over-correct, add layers of defensive code, and bury the actual bug.</p>
<p>The fix isn&#39;t a smarter model. The fix is closing the loop. The author needs to be able to <em>operate</em> what they author.</p>
<h2 id="the-mcp">The MCP</h2>
<p>We ship <code>@merchsage/mcp-kestra</code>, an MCP server that exposes the operations needed to close the loop. The tools fall into four buckets.</p>
<h3 id="authoring">Authoring</h3>
<table>
<thead>
<tr>
<th>Tool</th>
<th>What it does</th>
</tr>
</thead>
<tbody><tr>
<td><code>kestra_sync</code></td>
<td>Sync flows or namespace files to the Kestra server. Targets: <code>flow</code>, <code>namespace</code>, <code>all</code>.</td>
</tr>
</tbody></table>
<p>Authoring is the easy part. The hard part is what comes after.</p>
<h3 id="operating">Operating</h3>
<table>
<thead>
<tr>
<th>Tool</th>
<th>What it does</th>
</tr>
</thead>
<tbody><tr>
<td><code>kestra_run</code></td>
<td>Dispatch any flow with inputs. Optional <code>wait=True</code> polls every 10s.</td>
</tr>
<tr>
<td><code>kestra_pipeline_test</code></td>
<td>Cheap-test wrapper for the main pipeline. Selects stages, picks a small/fast channel, runs against the <code>onboarding_micro</code> config preset.</td>
</tr>
<tr>
<td><code>kestra_status</code></td>
<td>Status, stage progress, and artifact counts for an execution.</td>
</tr>
<tr>
<td><code>kestra_logs</code></td>
<td>Logs for an execution, filterable by level.</td>
</tr>
<tr>
<td><code>kestra_list</code></td>
<td>Recent executions for a flow.</td>
</tr>
</tbody></table>
<p><code>kestra_pipeline_test</code> is the one we&#39;d argue for hardest. Without a cheap-test wrapper, the model defaults to either &quot;I think it works&quot; or running a full pipeline that costs real money. With one, it dispatches a one-design micro-run, waits ~12 minutes, and reads the artifact counts. That&#39;s the development inner loop.</p>
<h3 id="inspecting-the-database">Inspecting (the database)</h3>
<table>
<thead>
<tr>
<th>Tool</th>
<th>What it does</th>
</tr>
</thead>
<tbody><tr>
<td><code>db_query</code></td>
<td>Read-only SQL. Auto-appends <code>LIMIT 50</code>. Returns formatted table.</td>
</tr>
<tr>
<td><code>db_schema</code></td>
<td>List tables or describe one table&#39;s columns/types.</td>
</tr>
<tr>
<td><code>db_channel</code></td>
<td>Look up a channel by UUID, handle (<code>@Name</code>), or YouTube channel ID.</td>
</tr>
<tr>
<td><code>db_artifacts</code></td>
<td>Count all pipeline artifacts for a channel.</td>
</tr>
<tr>
<td><code>db_execution</code></td>
<td>Status, stage progress, and artifact counts for an execution.</td>
</tr>
<tr>
<td><code>db_count</code> / <code>db_get</code> / <code>db_find</code></td>
<td>Fast typed queries for common shapes.</td>
</tr>
<tr>
<td><code>db_reset_channel</code></td>
<td>Delete all pipeline artifacts for a channel (dry-run by default).</td>
</tr>
</tbody></table>
<p>This is the half of the loop that&#39;s usually missing. A flow&#39;s logs tell you &quot;task X succeeded.&quot; The DB tells you whether task X <em>did the right thing</em>. With <code>db_query</code>, the agent can ask &quot;did this run actually write a <code>design_variants</code> row with non-null s3_key?&quot; instead of guessing from a green checkmark.</p>
<p><code>db_reset_channel</code> deserves a comment: it defaults to <code>dry_run=true</code> and prints what it would delete. We added the dry-run default after the model removed real channel data trying to &quot;clean up state.&quot; Defaults matter when an LLM is the one calling.</p>
<h2 id="codebase-contracts">Codebase contracts</h2>
<p>The MCP closes the loop. The codebase has to make the loop <em>useful</em>. Five contracts make agentic authoring tractable.</p>
<h3 id="1-thin-inline-python-in-flows">1. Thin inline Python in flows</h3>
<p>Business logic belongs in Python modules. Flow YAML contains input declarations, task wiring, env injection, and thin wrapper scripts (&lt;15 lines) that import a module function and call <code>emit_outputs</code>.</p>
<pre><code class="language-yaml"># GOOD
script: |
  import sys, os
  sys.path.insert(0, &quot;.&quot;)
  from merchsage.listing.seo import enrich_design_seo
  from merchsage.kestra import emit_outputs

  result = enrich_design_seo(
      design_id=os.environ[&quot;DESIGN_ID&quot;],
      channel_uuid=os.environ[&quot;CHANNEL_UUID&quot;],
  )
  emit_outputs(result)
</code></pre>
<p>200 lines of business logic in a YAML string is unreadable, untestable, and un-fixable for both a human and an agent. We mechanically resist that pattern.</p>
<h3 id="2-plugindefaults-injects-credentials">2. <code>pluginDefaults</code> injects credentials</h3>
<p>Every Python task gets credentials and <code>EXECUTION_ID</code> automatically through global <code>pluginDefaults</code>. Flow tasks don&#39;t declare <code>env:</code> for <code>GEMINI_TOKEN</code>, <code>DB_HOST</code>, <code>AWS_*</code>, etc. They only declare <code>env:</code> for dynamic, task-specific values like <code>CHANNEL_UUID</code>.</p>
<p>The agent doesn&#39;t have to remember which env vars to plumb. It writes <code>os.environ[&quot;GEMINI_TOKEN&quot;]</code> and it works. Reduces a whole class of &quot;I forgot to map this&quot; bugs.</p>
<h3 id="3-fail-fast-over-fallbacks">3. Fail fast over fallbacks</h3>
<p>This one is a behavioral contract more than an architectural one. If you let an LLM write code with no constraints, it will add try/except around every API call and fall back to defaults. Six months later, you&#39;ll have a pipeline that <em>appears</em> to work and silently produces wrong artifacts.</p>
<p>The codebase rule is: no synthetic data, no fallbacks, no backward-compatibility shims. Missing prompt? Crash. Missing required field? Crash. Mismatched fields? Skip with a warning. Required artifact missing? <code>sys.exit(1)</code>.</p>
<pre><code class="language-python"># BAD
prompt = get_prompt(&quot;my_prompt&quot;) or &quot;Some hardcoded fallback&quot;

# GOOD
prompt = get_prompt(&quot;my_prompt&quot;)  # raises PromptLoadError
</code></pre>
<p>A failed pipeline run is cheap. A silently-degraded run is not. This rule is how we keep agent-authored code debuggable.</p>
<h3 id="4-osenvironkey-not-getdefault">4. <code>os.environ[&quot;KEY&quot;]</code>, not <code>.get(default)</code></h3>
<p>Same principle, narrower instance. Flow YAML always provides declared env vars at runtime, so a Python-side default is dead code that masks missing configuration.</p>
<pre><code class="language-python"># BAD — hardcoded default masks missing config
region = os.environ.get(&quot;S3_REGION&quot;, &quot;eu-west-1&quot;)

# GOOD — KeyError if missing, fixed in seconds
region = os.environ[&quot;S3_REGION&quot;]
</code></pre>
<p>A <code>KeyError</code> with a clear name is a one-line fix. A wrong default that produces wrong artifacts is a week-long mystery.</p>
<h3 id="5-db-first-stage-handoff">5. DB-first stage handoff</h3>
<p>Stages don&#39;t pass outputs to one another through Kestra. They write to the DB and exit. The next stage loads what it needs by <code>channel_uuid</code>.</p>
<p>The big win is iteration. Once a stage has produced its output, you can re-run any downstream stage against that output as many times as you like — with different params, different prompts, or different code — without paying to regenerate the upstream work. A set of concepts can drive a dozen design experiments. A set of designs can produce mockups across several product configurations. The expensive upstream work is amortized across many cheap downstream variations. This is more than Kestra&#39;s task replay, which restarts a failed task in place — it&#39;s iterating on stage <em>logic</em> against a fixed upstream set.</p>
<p>Debuggability comes along for the ride. An agent investigating &quot;why did Stage 5 produce no mockups?&quot; can read the Stage 4 outputs from <code>design_variants</code> directly, without replaying Stage 4 or parsing an <code>outputs</code> JSON blob in a Kestra log. The DB is the audit trail.</p>
<h2 id="what-this-unlocks">What this unlocks</h2>
<p>When the loop is closed and the contracts hold, the agent goes from writing code to operating the system. Concretely:</p>
<ul>
<li>It writes a new flow, syncs it, dispatches a micro-run, reads <code>kestra_logs</code>, finds a <code>KeyError</code> on a missing env, fixes it, re-syncs, re-runs.</li>
<li>It investigates &quot;why did this channel only get 1 design instead of 8?&quot; by <code>db_query</code>-ing <code>design_variants</code> joined to <code>product_concepts</code>, finding the rating threshold rejected 7 of them, and adjusting the config preset.</li>
<li>It diagnoses a stuck <code>phase_creative</code> execution by reading <code>kestra_status</code>, killing it, calling <code>db_reset_channel</code> with <code>dry_run=true</code> first to confirm what it&#39;ll delete, then running it for real.</li>
</ul>
<p>None of those steps require the agent to ask &quot;what should I do?&quot; The information it needs is directly accessible by tool call.</p>
<h2 id="the-point">The point</h2>
<p>Two things make this work, and they&#39;re co-dependent. The MCP closes the operating loop — author, run, observe, inspect, reset. The codebase contracts make the signals from that loop trustworthy — fail loudly, never fall back, never silently degrade.</p>
<p>The same setup now runs the merch pipeline, the ops automation, and the marketing flows — with the humans focused on product and architecture, not the code.</p>
]]></content:encoded>
            <category>kestra</category>
            <category>llm</category>
            <category>mcp</category>
            <category>agents</category>
            <category>claude-code</category>
        </item>
        <item>
            <title><![CDATA[How MerchSage turns a YouTube channel into print-on-demand merch in 6 stages]]></title>
            <link>https://merchsage.com/blog/youtube-to-merch-pipeline</link>
            <guid>https://merchsage.com/blog/youtube-to-merch-pipeline</guid>
            <pubDate>Fri, 08 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[An overview of MerchSage's autonomous pipeline: Scrape → Analyze → Concepts → Designs → Mockups → Listings. What each stage contributes and how they fit together.]]></description>
            <content:encoded><![CDATA[<p>MerchSage takes a YouTube channel URL and produces a stocked storefront. No human picks the products. No human writes artwork prompts. No human approves a design before it lands on a t-shirt. The whole thing runs as a 6-stage pipeline.</p>
<p>This post is the overview — what each stage contributes and how they fit together. Two of the stages are interesting enough to deserve their own posts, linked below.</p>
<h2 id="the-6-stages">The 6 stages</h2>
<pre><code>Scrape → Analyze → Generate Concepts → Create Designs → Generate Mockups → Finalize Listings
</code></pre>
<table>
<thead>
<tr>
<th>Stage</th>
<th>Role</th>
</tr>
</thead>
<tbody><tr>
<td>1. Scrape</td>
<td>Pull raw material from YouTube</td>
</tr>
<tr>
<td>2. Analyze</td>
<td>Build a structured creative brief for the channel</td>
</tr>
<tr>
<td>3. Concepts</td>
<td>Turn the brief into design briefs</td>
</tr>
<tr>
<td>4. Designs</td>
<td>Render the briefs as artwork</td>
</tr>
<tr>
<td>5. Mockups</td>
<td>Show the artwork on real products</td>
</tr>
<tr>
<td>6. Listings</td>
<td>Produce storefront-ready listing drafts</td>
</tr>
</tbody></table>
<p>Each stage has one job. Each stage can be re-run on its own.</p>
<h2 id="stage-1--scrape">Stage 1 — Scrape</h2>
<p>We pull the raw evidence: a representative sample of the channel&#39;s videos, their transcripts, comments, and visual material. The sample is time-spread and outlier-trimmed, so a single viral hit doesn&#39;t drag the brand reading off-center. This is the only stage that reaches outside the system — everything downstream works from what we capture here.</p>
<h2 id="stage-2--analyze">Stage 2 — Analyze</h2>
<p>A set of AI specialists read the scraped material and build a structured understanding of the channel. Between them, they produce:</p>
<ul>
<li><strong>Brand understanding</strong> — what the creator stands for, who the audience is, the personality of the channel.</li>
<li><strong>A visual design guide</strong> — palette, motifs, typography, the creative range that fits the brand.</li>
<li><strong>A product plan</strong> — which products belong in this creator&#39;s lineup, and what mockup scenes match the channel&#39;s vibe.</li>
<li><strong>Asset extraction</strong> — recurring visual elements (logos, faces, characters) lifted from channel imagery for reuse in designs.</li>
</ul>
<p>Every downstream creative decision flows from this. Anything we generate later sits on top of the design guide and the product plan.</p>
<h2 id="stage-3--generate-concepts">Stage 3 — Generate Concepts</h2>
<p>Turn the creative brief into design briefs — the specifications that drive image generation.</p>
<p>We generate aggressively, far more concepts than we&#39;ll keep, to maximize creative diversity. A rating-and-pruning pass then selects the strongest, most distinctive ones to actually render.</p>
<p>The split between generation and selection is deliberate. Asking a model to be both wildly creative and ruthlessly discerning in a single pass produces safe, average output. Splitting it lets generation run uninhibited and selection run cold.</p>
<h2 id="stage-4--create-designs">Stage 4 — Create Designs</h2>
<p>Render the selected briefs into transparent artwork — ready to drop onto a product.</p>
<p>The hard part isn&#39;t the image generation itself — it&#39;s getting clean transparent cutouts reliably, on every design, at scale. That&#39;s its own <a href="/blog/clean-cutouts-from-generative-models">post</a>.</p>
<p>A rating pass scores the rendered designs. Anything the model bungled — visible artifacts, broken composition, off-brand colors — gets filtered before it ever reaches a product.</p>
<h2 id="stage-5--generate-mockups">Stage 5 — Generate Mockups</h2>
<p>Send each design to <a href="https://www.printful.com">Printful</a> to be rendered on real products — t-shirts, posters, mugs, phone cases — in scenes chosen to match the channel&#39;s vibe. A visual quality gate scores how well each design sits on its product. Mockups that don&#39;t pass are kept for review but excluded from the storefront.</p>
<h2 id="stage-6--finalize-listings">Stage 6 — Finalize Listings</h2>
<p>Pick the best design per product line, write SEO-ready listing copy, and produce drafts for the storefront. Publishing happens later — admin curation and the creator&#39;s own selection through the portal control what actually goes live.</p>
<h2 id="how-the-stages-fit-together">How the stages fit together</h2>
<p>Stages communicate through a shared database, not through pipeline outputs. Each stage persists everything it produces; the next stage loads what it needs. That decoupling is what makes any stage re-runnable in isolation, and what lets a failed stage stop a run cleanly without taking the rest of the pipeline with it.</p>
<p>The orchestration itself — how the stages are wired together, how concurrent pipeline runs share rate-limited APIs, how an LLM agent can author and operate the whole thing — is its own <a href="/blog/agentic-kestra">post</a>.</p>
<p>A failed run is cheap. A silently-degraded run produces wrong artifacts. The whole codebase leans hard into the first.</p>
]]></content:encoded>
            <category>pipeline</category>
            <category>architecture</category>
            <category>llm</category>
            <category>print-on-demand</category>
        </item>
    </channel>
</rss>