<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Api-Throttling on ICE-ICE-BEAR-BLOG</title><link>https://ice-ice-bear.github.io/tags/api-throttling/</link><description>Recent content in Api-Throttling on ICE-ICE-BEAR-BLOG</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Fri, 03 Apr 2026 00:00:00 +0900</lastBuildDate><atom:link href="https://ice-ice-bear.github.io/tags/api-throttling/index.xml" rel="self" type="application/rss+xml"/><item><title>PopCon Dev Log #2 — Branding, README, Docker Debugging, and Retry Logic</title><link>https://ice-ice-bear.github.io/posts/2026-04-03-popcon-dev2/</link><pubDate>Fri, 03 Apr 2026 00:00:00 +0900</pubDate><guid>https://ice-ice-bear.github.io/posts/2026-04-03-popcon-dev2/</guid><description>&lt;img src="https://ice-ice-bear.github.io/" alt="Featured image of post PopCon Dev Log #2 — Branding, README, Docker Debugging, and Retry Logic" /&gt;&lt;p&gt;&lt;a class="link" href="https://ice-ice-bear.github.io/ko/posts/2026-04-02-popcon-dev1/" &gt;Previous: PopCon Dev Log #1&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="overview"&gt;Overview
&lt;/h2&gt;&lt;p&gt;Today&amp;rsquo;s session split between polishing the &amp;ldquo;outside&amp;rdquo; and hardening the &amp;ldquo;inside&amp;rdquo; of PopCon. The morning was branding — turning a Gemini-generated image into logo and favicon assets, then writing a GitHub-ready README. The afternoon turned into a Docker debugging session that led to pipeline quality improvements, finishing with retry logic and per-emoji error handling.&lt;/p&gt;
&lt;h2 id="1-logo--favicon--from-gemini-image-to-brand-assets"&gt;1. Logo &amp;amp; Favicon — From Gemini Image to Brand Assets
&lt;/h2&gt;&lt;p&gt;The first task was converting a 2880×1440 Gemini-generated image into PopCon brand assets. The image was center-cropped to 1:1 then resized into multiple sizes.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;File&lt;/th&gt;
 &lt;th&gt;Size&lt;/th&gt;
 &lt;th&gt;Purpose&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;logo.png&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;512×512&lt;/td&gt;
 &lt;td&gt;Header logo&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;favicon.ico&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;16/32/48 multi-size&lt;/td&gt;
 &lt;td&gt;Browser tab icon&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;favicon-16x16.png&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;16×16&lt;/td&gt;
 &lt;td&gt;Small favicon&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;favicon-32x32.png&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;32×32&lt;/td&gt;
 &lt;td&gt;Standard favicon&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;apple-touch-icon.png&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;180×180&lt;/td&gt;
 &lt;td&gt;iOS home screen&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;icon-192.png&lt;/code&gt; / &lt;code&gt;icon-512.png&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;192×192 / 512×512&lt;/td&gt;
 &lt;td&gt;PWA icons&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="why-the-favicon-wasnt-showing-in-docker"&gt;Why the Favicon Wasn&amp;rsquo;t Showing in Docker
&lt;/h3&gt;&lt;p&gt;Two issues stacked on each other.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Next.js App Router priority&lt;/strong&gt;: &lt;code&gt;app/favicon.ico&lt;/code&gt; takes precedence over &lt;code&gt;public/favicon.ico&lt;/code&gt;. A default favicon was already sitting in &lt;code&gt;app/&lt;/code&gt; and needed to be replaced there.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker image baking&lt;/strong&gt;: The Dockerfile uses &lt;code&gt;COPY . .&lt;/code&gt; at build time. Changing files on disk has no effect until the container is rebuilt.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose build frontend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker compose up -d frontend
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Confirmed with &lt;code&gt;curl -I localhost:3000/favicon.ico&lt;/code&gt; returning HTTP 200.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="2-full-product-readme"&gt;2. Full Product README
&lt;/h2&gt;&lt;p&gt;Next came writing a README that reads like a product landing page rather than a bare technical doc.&lt;/p&gt;
&lt;p&gt;The first version put both English and Korean in a single file. Feedback: &amp;ldquo;the two languages aren&amp;rsquo;t distinguishable.&amp;rdquo; Split them into separate files with language toggle links at the top of each.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;README.md&lt;/code&gt; — English, with &lt;code&gt;English | [한국어](README.ko.md)&lt;/code&gt; at the top&lt;/li&gt;
&lt;li&gt;&lt;code&gt;README.ko.md&lt;/code&gt; — Korean, with &lt;code&gt;[English](README.md) | 한국어&lt;/code&gt; at the top&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="readme-vs-reality"&gt;README vs. Reality
&lt;/h3&gt;&lt;p&gt;After the first commit, reviewing the actual code revealed several discrepancies.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Item&lt;/th&gt;
 &lt;th&gt;What the README said&lt;/th&gt;
 &lt;th&gt;What the code does&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Image generation model&lt;/td&gt;
 &lt;td&gt;Google Imagen&lt;/td&gt;
 &lt;td&gt;Gemini Flash Image&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;VEO mode&lt;/td&gt;
 &lt;td&gt;Dual-frame I2V&lt;/td&gt;
 &lt;td&gt;Start frame + motion prompt only (API limitation)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Video duration&lt;/td&gt;
 &lt;td&gt;&amp;ldquo;under 4 seconds&amp;rdquo;&lt;/td&gt;
 &lt;td&gt;Exactly 4s (API minimum), trimmed in post-processing&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Preprocessing step&lt;/td&gt;
 &lt;td&gt;Not mentioned&lt;/td&gt;
 &lt;td&gt;Crop → square pad → resize to 512×512&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Job persistence&lt;/td&gt;
 &lt;td&gt;Not mentioned&lt;/td&gt;
 &lt;td&gt;Redis with 24-hour TTL&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Missing endpoint&lt;/td&gt;
 &lt;td&gt;Not listed&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;/api/job/{job_id}/emoji/{filename}&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Both README files updated and pushed.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="3-docker-debugging--fighting-the-api-key"&gt;3. Docker Debugging — Fighting the API Key
&lt;/h2&gt;&lt;p&gt;The afternoon session opened with Docker logs showing nothing working. The root cause was a trailing &lt;code&gt; venv&lt;/code&gt; appended to the API key in &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;POPCON_GOOGLE_API_KEY=AIzaSy...-mAcuv venv
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Likely copied from a terminal where the &lt;code&gt;venv&lt;/code&gt; activation command ran next. Removed the trailing text, restarted — but the key itself turned out to be expired too. Generated a fresh one from Google AI Studio.&lt;/p&gt;
&lt;p&gt;Running the pipeline for real surfaced a series of quality problems.&lt;/p&gt;
&lt;h3 id="issues-found-and-fixed"&gt;Issues Found and Fixed
&lt;/h3&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TD
 A["Pipeline run"] --&gt; B["Review output"]
 B --&gt; C["Problems found"]
 C --&gt; D["Cloud artifacts &amp;lt;br/&amp;gt; in background"]
 C --&gt; E["Duplicate characters &amp;lt;br/&amp;gt; sprite sheets"]
 C --&gt; F["Checkerboard background &amp;lt;br/&amp;gt; NB2 fake transparency"]
 C --&gt; G["VEO edge lines &amp;lt;br/&amp;gt; left/right borders"]
 D --&gt; H["Remove rembg entirely &amp;lt;br/&amp;gt; brightness-based crop instead"]
 E --&gt; I["Prompt: single character only &amp;lt;br/&amp;gt; no sticker sheets"]
 F --&gt; J["Prompt: #FFFFFF background &amp;lt;br/&amp;gt; NOT checkerboard"]
 G --&gt; K["ffmpeg 2% edge crop"]&lt;/pre&gt;&lt;p&gt;The biggest decision was removing &lt;code&gt;rembg&lt;/code&gt; entirely. Every attempt at background removal made things worse — &lt;code&gt;isnet-general-use&lt;/code&gt; left cloud artifacts, &lt;code&gt;u2net&lt;/code&gt; wasn&amp;rsquo;t better. Instead: prompt VEO to generate white backgrounds, then use brightness-based cropping to extract content.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# processor.py — brightness-based content detection&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;brightness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;content_mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;brightness&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;brightness&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;245&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Removing &lt;code&gt;rembg[cpu]&amp;gt;=2.0.0&lt;/code&gt; from &lt;code&gt;pyproject.toml&lt;/code&gt; and replacing it with &lt;code&gt;numpy&amp;gt;=1.26.0&lt;/code&gt; also slimmed the Docker image.&lt;/p&gt;
&lt;h3 id="line-file-naming-fix"&gt;LINE File Naming Fix
&lt;/h3&gt;&lt;p&gt;Checking the LINE Creators Market guidelines: files must be named &lt;code&gt;001.png&lt;/code&gt; through &lt;code&gt;040.png&lt;/code&gt;. The code was saving them by action name.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# packager.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;emoji_path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emoji_paths&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;line_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;03d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.png&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;zf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emoji_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="4-retry-logic-and-api-throttling"&gt;4. Retry Logic and API Throttling
&lt;/h2&gt;&lt;p&gt;The final commit focused on resilience. Generating a full 24-emoji set means many API calls to VEO and Gemini — and they occasionally return 503 or 429. Previously, one failure killed the whole job.&lt;/p&gt;
&lt;h3 id="per-emoji-error-handling"&gt;Per-Emoji Error Handling
&lt;/h3&gt;&lt;p&gt;The fix wraps each emoji&amp;rsquo;s pipeline stages in a try/except, marks failures with &lt;code&gt;&amp;quot;error&amp;quot;&lt;/code&gt; status, and continues with the rest.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# worker.py — per-emoji error handling&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;failed_indices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ... pose generation, animation, post-processing&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Emoji &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;) failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;failed_indices&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;error&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;save_job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A new &lt;code&gt;&amp;quot;done_with_errors&amp;quot;&lt;/code&gt; job status means the ZIP is still available even if some emojis failed.&lt;/p&gt;
&lt;h3 id="api-retry-with-exponential-backoff"&gt;API Retry with Exponential Backoff
&lt;/h3&gt;&lt;p&gt;Both the Gemini Image and VEO calls now retry up to three times on transient errors.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# pose_generator.py — retry logic&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_generate_image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reference_image_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generate_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ServerError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ClientError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="c1"&gt;# 1s, 2s, 4s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Attempt &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; failed, retrying in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="type-system-sync"&gt;Type System Sync
&lt;/h3&gt;&lt;p&gt;Status types updated across backend and frontend.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Layer&lt;/th&gt;
 &lt;th&gt;Change&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;backend/models.py&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Added &lt;code&gt;&amp;quot;error&amp;quot;&lt;/code&gt; to &lt;code&gt;EmojiStatus&lt;/code&gt;, &lt;code&gt;&amp;quot;done_with_errors&amp;quot;&lt;/code&gt; to &lt;code&gt;JobStatusType&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;frontend/lib/api.ts&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Mirrored the same status union types&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;frontend/components/ProgressTracker.tsx&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Red card UI for &lt;code&gt;&amp;quot;error&amp;quot;&lt;/code&gt; status emojis&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;frontend/components/EmojiPreview.tsx&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Show ZIP download button on &lt;code&gt;&amp;quot;done_with_errors&amp;quot;&lt;/code&gt; too&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="summary"&gt;Summary
&lt;/h2&gt;&lt;p&gt;The four commits today in order:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;#&lt;/th&gt;
 &lt;th&gt;Work done&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;1&lt;/td&gt;
 &lt;td&gt;Logo/favicon assets, branding, full product README&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2&lt;/td&gt;
 &lt;td&gt;Split README into English and Korean with language toggle&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;3&lt;/td&gt;
 &lt;td&gt;Updated READMEs to match actual pipeline behavior&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;4&lt;/td&gt;
 &lt;td&gt;Retry logic, per-emoji error handling, API throttling&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The Docker debugging session was unexpectedly productive — it forced a real pipeline run that surfaced quality issues, and the decision to remove &lt;code&gt;rembg&lt;/code&gt; entirely turned out to be the right call. Less code, smaller Docker image, cleaner output.&lt;/p&gt;
&lt;p&gt;Next up: final quality validation before submitting to LINE Creators Market.&lt;/p&gt;</description></item></channel></rss>