<?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: TuTuTu</title>
    <description>The latest articles on DEV Community by TuTuTu (@tutututu).</description>
    <link>https://dev.to/tutututu</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%2F4072186%2F7c4f86eb-49ae-4f07-8d17-9df53b513bdd.png</url>
      <title>DEV Community: TuTuTu</title>
      <link>https://dev.to/tutututu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tutututu"/>
    <language>en</language>
    <item>
      <title>AI Tattoo Generator: How I Built a Placement-Aware Design Workflow</title>
      <dc:creator>TuTuTu</dc:creator>
      <pubDate>Tue, 11 Aug 2026 03:29:40 +0000</pubDate>
      <link>https://dev.to/tutututu/ai-tattoo-generator-how-i-built-a-placement-aware-design-workflow-1c2k</link>
      <guid>https://dev.to/tutututu/ai-tattoo-generator-how-i-built-a-placement-aware-design-workflow-1c2k</guid>
      <description>&lt;p&gt;The obvious architecture for an AI image product looks almost too simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;form -&amp;gt; prompt -&amp;gt; image model -&amp;gt; result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is enough for a demo. It was not enough for the tattoo planning product I&lt;br&gt;
wanted to build.&lt;/p&gt;

&lt;p&gt;A generic image model can make a beautiful square illustration while ignoring&lt;br&gt;
the things that make a tattoo concept useful: body placement, readable line&lt;br&gt;
weight, negative space, silhouette, scale, privacy, and a clean handoff to a&lt;br&gt;
professional artist.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;AI tattoo generator&lt;/strong&gt; is a tool that turns a written idea and design&lt;br&gt;
constraints into a tattoo concept. The important word is &lt;em&gt;concept&lt;/em&gt;. The output&lt;br&gt;
can help someone explore a direction and communicate it, but it is not a final&lt;br&gt;
stencil or a substitute for an artist adapting the design to real skin.&lt;/p&gt;

&lt;p&gt;This post explains the production decisions behind the placement-aware&lt;br&gt;
workflow I built for&lt;br&gt;
&lt;a href="https://tattooideas.app/ai-tattoo-generator" rel="noopener noreferrer"&gt;TattooIdeas.app&lt;/a&gt;. I am part of&lt;br&gt;
the team behind the product, so this is an engineering retrospective, not an&lt;br&gt;
independent review.&lt;/p&gt;
&lt;h2&gt;
  
  
  The real workflow is longer than one model call
&lt;/h2&gt;

&lt;p&gt;The product journey eventually became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Explore -&amp;gt; Describe -&amp;gt; Constrain -&amp;gt; Generate -&amp;gt; Refine -&amp;gt; Preview -&amp;gt; Artist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each arrow hides a different problem. The model needs a precise brief. The&lt;br&gt;
request needs to survive a long-running job. The output must become a private,&lt;br&gt;
durable project file. The artwork needs to work in a placement preview. And the&lt;br&gt;
user needs to understand where software stops and professional judgment begins.&lt;/p&gt;

&lt;p&gt;The current stack looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;React 19 UI
  -&amp;gt; TanStack Start server function
     -&amp;gt; Cloudflare Worker
        -&amp;gt; D1 job record + credit ledger
        -&amp;gt; image provider queue
        &amp;lt;- signed webhook / status reconciliation
        -&amp;gt; private result copied to R2
  &amp;lt;- TanStack Query refreshes the job state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TanStack Start gives the app server rendering and typed server functions.&lt;br&gt;
Cloudflare Workers runs the application layer, D1 stores jobs and project&lt;br&gt;
metadata, and R2 owns the generated files. The image provider performs the&lt;br&gt;
expensive inference, but it does not own the product state.&lt;/p&gt;

&lt;p&gt;Here are the lessons that made the largest difference.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Compile a prompt instead of concatenating one
&lt;/h2&gt;

&lt;p&gt;A single textarea is flexible, but it leaves too many decisions implicit.&lt;br&gt;
“A crane tattoo” does not say whether the user wants fine-line or blackwork,&lt;br&gt;
an inner-forearm composition or a back piece, restrained detail or dense&lt;br&gt;
ornament, black ink or color.&lt;/p&gt;

&lt;p&gt;I model those decisions as typed inputs:&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;TattooPromptInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;prompt&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;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TattooStyleId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;placement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TattooPlacementId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TattooColorId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;complexity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TattooComplexityId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;aspectRatio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&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="s1"&gt;1:1&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="s1"&gt;2:3&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="s1"&gt;3:2&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 UI collects them separately, and a prompt compiler turns them into one&lt;br&gt;
model brief. A simplified version 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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildTattooPrompt&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="nx"&gt;TattooPromptInput&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;idea&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encodeUserIdea&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="nx"&gt;prompt&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;ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getTattooAspectRatio&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="nx"&gt;placement&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="nx"&gt;aspectRatio&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Create exactly one original tattoo design concept.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`&amp;lt;USER_IDEA&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;idea&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/USER_IDEA&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`STYLE: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;getStyleDirection&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="nx"&gt;style&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="s2"&gt;`PLACEMENT: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;getPlacementDirection&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="nx"&gt;placement&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="s2"&gt;`COLOR: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;getColorDirection&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="nx"&gt;color&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="s2"&gt;`DETAIL: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;getComplexityDirection&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="nx"&gt;complexity&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="s2"&gt;`COMPOSITION: use a &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ratio&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; canvas with a complete silhouette`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Use purposeful line weights, durable contrast, and negative space.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Show isolated artwork on a plain background. Do not render skin.&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n\n&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;There are two useful ideas here.&lt;/p&gt;

&lt;p&gt;First, structured controls make the request reproducible. If a user changes&lt;br&gt;
only the style, the rest of the creative brief remains stable. That makes&lt;br&gt;
comparison and refinement much easier.&lt;/p&gt;

&lt;p&gt;Second, user text is data, not authority. I escape it, place it inside a clear&lt;br&gt;
boundary, and explicitly tell the model how to interpret conflicts. Boundary&lt;br&gt;
tags are not a security mechanism by themselves, but they reduce accidental&lt;br&gt;
instruction mixing when combined with validation and a fixed higher-priority&lt;br&gt;
brief.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Placement should change composition, not decorate metadata
&lt;/h2&gt;

&lt;p&gt;Many AI image forms include a “body part” dropdown and then barely use it. I&lt;br&gt;
found placement useful only when it affected the geometry of the requested&lt;br&gt;
artwork.&lt;/p&gt;

&lt;p&gt;An inner forearm usually benefits from a vertical, readable silhouette. A&lt;br&gt;
shoulder can support a more radial composition. A sleeve needs continuity and&lt;br&gt;
flow rather than a centered sticker. Placement can therefore choose a default&lt;br&gt;
aspect ratio and add composition guidance before the request reaches the&lt;br&gt;
model.&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;getTattooAspectRatio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;placement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requested&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&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="nx"&gt;requested&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&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;requested&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;placementOptions&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="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="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;placement&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;aspectRatio&lt;/span&gt;
    &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1:1&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;This does not make the model understand anatomy. It simply turns a vague label&lt;br&gt;
into useful constraints: dominant direction, visual weight, focal hierarchy,&lt;br&gt;
margin, and crop safety.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7629radc1nm8vomitbwt.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7629radc1nm8vomitbwt.webp" alt="Three crane and moon tattoo concepts shown in fine-line, blackwork, and Japanese-inspired visual directions" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;
One subject can produce very different concepts when style and
composition are explicit rather than buried in a long free-form prompt.



&lt;p&gt;The image above also shows why I prefer controlled variation to random&lt;br&gt;
regeneration. A useful comparison keeps the subject recognizable while&lt;br&gt;
changing one meaningful dimension.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Long-running generation is a state machine
&lt;/h2&gt;

&lt;p&gt;The first production problem has little to do with image quality. Generation&lt;br&gt;
takes long enough for requests to time out, tabs to close, providers to retry&lt;br&gt;
webhooks, and users to click twice.&lt;/p&gt;

&lt;p&gt;I store a job before submitting inference and treat these states as part of&lt;br&gt;
the product:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;queued -&amp;gt; processing -&amp;gt; succeeded
                    \-&amp;gt; failed
                    \-&amp;gt; canceled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The start endpoint validates the input, reserves a job, records the charge,&lt;br&gt;
and submits the provider request in background work. A webhook can reconcile&lt;br&gt;
the result, while the client can also request status if the webhook is late.&lt;br&gt;
Both paths call the same reconciliation logic.&lt;/p&gt;

&lt;p&gt;The important detail is idempotency. Charging, refunding, and persisting output&lt;br&gt;
must all be safe when the same event arrives more than once.&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;providerStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;providerStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canceled&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;refundGenerationCredits&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;generationId&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="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;job&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="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;markJobTerminal&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="nx"&gt;providerStatus&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;In the real implementation, the credit ledger uses a stable generation ID so&lt;br&gt;
a repeated failure event cannot create a repeated refund. Abandoned jobs are&lt;br&gt;
also reconciled after a timeout. “The provider failed” should be a recoverable&lt;br&gt;
state, not a support ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A provider URL is not your storage layer
&lt;/h2&gt;

&lt;p&gt;Image APIs commonly return temporary delivery URLs. Saving that URL in a&lt;br&gt;
database is tempting, but it makes retention, access control, and deletion&lt;br&gt;
someone else's responsibility.&lt;/p&gt;

&lt;p&gt;When a result succeeds, the Worker:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;accepts only an HTTPS output URL;&lt;/li&gt;
&lt;li&gt;verifies the response type is JPEG, PNG, or WebP;&lt;/li&gt;
&lt;li&gt;enforces declared and actual byte limits;&lt;/li&gt;
&lt;li&gt;copies the image to a private R2 object;&lt;/li&gt;
&lt;li&gt;creates file and design records owned by the user; and&lt;/li&gt;
&lt;li&gt;returns an authenticated application URL instead of the provider URL.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That extra copy is where a transient model output becomes a product artifact.&lt;br&gt;
It also means the app can implement deletion and access rules consistently&lt;br&gt;
across multiple inference providers.&lt;/p&gt;

&lt;p&gt;This ownership boundary is especially important for image-to-image workflows.&lt;br&gt;
A pet photo, a handwritten note, or a photo of an existing tattoo may be far&lt;br&gt;
more sensitive than a text prompt. Generated projects in this app are private&lt;br&gt;
by default, and the UI states when an uploaded source is stored versus&lt;br&gt;
processed locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Placement preview is a graphics problem of its own
&lt;/h2&gt;

&lt;p&gt;Generating isolated artwork and making it look plausible on a curved body are&lt;br&gt;
different tasks. I deliberately keep them separate.&lt;/p&gt;

&lt;p&gt;The first step happens in the browser. Generated artwork often has a white&lt;br&gt;
background, but removing every light pixel would also erase intentional white&lt;br&gt;
highlights inside the design. Instead, a flood fill removes only a light,&lt;br&gt;
neutral region connected to the image border, with a soft alpha transition at&lt;br&gt;
the edge.&lt;/p&gt;

&lt;p&gt;The preview renderer then combines four textures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the tattoo artwork;&lt;/li&gt;
&lt;li&gt;a depth map;&lt;/li&gt;
&lt;li&gt;a skin mask; and&lt;/li&gt;
&lt;li&gt;a body-surface map.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A small WebGL shader adjusts the artwork using local depth gradients and&lt;br&gt;
surface continuity. Users can move, scale, rotate, and change opacity without&lt;br&gt;
running another AI request. If WebGL or a texture fails, the interface falls&lt;br&gt;
back to a simpler CSS contour transform instead of making the whole workflow&lt;br&gt;
unavailable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqwo4ht4crgp9en3fwdbd.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqwo4ht4crgp9en3fwdbd.webp" alt="A side-by-side forearm preview with bare skin on the left and a botanical snake tattoo concept on the right" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;
A placement preview is useful for comparing scale and direction.
It is not a simulation of healing, ink spread, or how a specific body moves.



&lt;p&gt;The fallback matters more than the shader. An advanced preview is an&lt;br&gt;
enhancement; it should never block someone from downloading a concept or&lt;br&gt;
continuing the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The model output needs a human last mile
&lt;/h2&gt;

&lt;p&gt;The most important product decision was refusing to describe the result as&lt;br&gt;
“tattoo-ready.”&lt;/p&gt;

&lt;p&gt;A screen cannot inspect skin, choose a needle grouping, predict healing, or&lt;br&gt;
decide how fine detail will age at a particular size. A professional tattoo&lt;br&gt;
artist still needs to redraw and adapt the concept for anatomy and technique.&lt;/p&gt;

&lt;p&gt;The generator can make that conversation better. A useful artist handoff&lt;br&gt;
includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the meaning and must-have elements;&lt;/li&gt;
&lt;li&gt;the intended body area and approximate size;&lt;/li&gt;
&lt;li&gt;two or three visual directions rather than one demanded copy;&lt;/li&gt;
&lt;li&gt;notes about preferred line weight, contrast, and color; and&lt;/li&gt;
&lt;li&gt;explicit permission for the artist to simplify and redesign.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The prompt compiler also rejects the idea that selecting a style means copying&lt;br&gt;
a living artist's recognizable composition. Style controls should describe a&lt;br&gt;
visual language—fine-line, blackwork, geometric, neo-traditional—not impersonate&lt;br&gt;
someone's portfolio.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would build first next time
&lt;/h2&gt;

&lt;p&gt;If I were starting another production image-AI tool, I would implement these&lt;br&gt;
pieces before adding more models:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;a typed input model for the decisions users actually understand;&lt;/li&gt;
&lt;li&gt;a prompt compiler with clear user-content boundaries;&lt;/li&gt;
&lt;li&gt;a persisted, idempotent async job state machine;&lt;/li&gt;
&lt;li&gt;automatic refunds or retries for terminal failures;&lt;/li&gt;
&lt;li&gt;private application-owned storage for results;&lt;/li&gt;
&lt;li&gt;a non-AI last-mile editor or preview; and&lt;/li&gt;
&lt;li&gt;honest language about what still requires human expertise.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Model quality matters, but reliability and constraint design are what turn a&lt;br&gt;
model call into a product.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Is an AI tattoo generator different from a general image generator?
&lt;/h3&gt;

&lt;p&gt;The underlying model may be similar. The useful difference is the workflow:&lt;br&gt;
tattoo-specific style, placement, color, detail, composition, isolated artwork,&lt;br&gt;
project storage, and artist handoff constraints.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is an AI-generated tattoo design ready to tattoo?
&lt;/h3&gt;

&lt;p&gt;No. Treat it as a concept or visual brief. A qualified artist should adapt the&lt;br&gt;
line weight, spacing, scale, contrast, and composition for the body and for&lt;br&gt;
long-term readability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do users need drawing skills?
&lt;/h3&gt;

&lt;p&gt;No. Structured text controls help someone express subject, meaning, style, and&lt;br&gt;
placement. Being able to explain what matters is more useful than being able&lt;br&gt;
to draw a polished sketch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does virtual try-on prove that a placement will work?
&lt;/h3&gt;

&lt;p&gt;No. It can help compare approximate size, orientation, and visibility. It&lt;br&gt;
cannot predict movement, healing, fading, or the feasibility of a cover-up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;Building this changed how I think about image AI. The inference call is the&lt;br&gt;
smallest interesting part of the system. The real product lives in the&lt;br&gt;
constraints before the call, the reliability around it, and the decisions a&lt;br&gt;
human can make afterward.&lt;/p&gt;

&lt;p&gt;That is true for tattoos, but it also applies to fashion previews, interior&lt;br&gt;
design tools, product mockups, and almost every workflow where a generated&lt;br&gt;
image has to survive contact with the physical world.&lt;/p&gt;

&lt;p&gt;If you have built a production image-AI tool, what turned out to be your&lt;br&gt;
hardest “last mile” problem?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tattoo</category>
    </item>
  </channel>
</rss>
