<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Internal Tools on ICE-ICE-BEAR-BLOG</title><link>https://ice-ice-bear.github.io/tags/internal-tools/</link><description>Recent content in Internal Tools on ICE-ICE-BEAR-BLOG</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Thu, 07 May 2026 00:00:00 +0900</lastBuildDate><atom:link href="https://ice-ice-bear.github.io/tags/internal-tools/index.xml" rel="self" type="application/rss+xml"/><item><title>hybrid-image-search Dev Log #18 — OpenAI gpt-image-2 Joins, Model/Product Library, and Internal Permission Tiers</title><link>https://ice-ice-bear.github.io/posts/2026-05-07-hybrid-search-dev18/</link><pubDate>Thu, 07 May 2026 00:00:00 +0900</pubDate><guid>https://ice-ice-bear.github.io/posts/2026-05-07-hybrid-search-dev18/</guid><description>&lt;img src="https://ice-ice-bear.github.io/" alt="Featured image of post hybrid-image-search Dev Log #18 — OpenAI gpt-image-2 Joins, Model/Product Library, and Internal Permission Tiers" /&gt;&lt;h2 id="overview"&gt;Overview
&lt;/h2&gt;&lt;p&gt;Since &lt;a class="link" href="https://ice-ice-bear.github.io/posts/2026-04-22-hybrid-search-dev17/" &gt;#17 — tone pool swaps, model injection prompt v2&lt;/a&gt;, 73 commits have landed. The biggest shift is &lt;strong&gt;dropping the injection-mode abstraction itself&lt;/strong&gt; — what used to be a five-tone pill row collapsed into two tabs: model and product. At the same time, we started routing the comparison side B through OpenAI&amp;rsquo;s gpt-image-2.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 Old["Through dev #17 &amp;lt;br/&amp;gt; injection-mode pills (5 tones)"] --&gt; Refactor["Drop pills &amp;lt;br/&amp;gt; model / product tabs"]
 Refactor --&gt; A["Side A: Gemini 3.1 Flash &amp;lt;br/&amp;gt; (primary)"]
 Refactor --&gt; B["Side B: OpenAI gpt-image-2 &amp;lt;br/&amp;gt; (comparison)"]
 Library["Per-user library &amp;lt;br/&amp;gt; model + product"] --&gt; A
 Library --&gt; B
 Internal["Internal permission gate &amp;lt;br/&amp;gt; tone-lock + S3 admin"] --&gt; A&lt;/pre&gt;&lt;p&gt;73 commits, five threads.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="openai-gpt-image-2-lands-as-side-b"&gt;OpenAI gpt-image-2 lands as side B
&lt;/h2&gt;&lt;p&gt;Until now hybrid was a single-backend (Gemini) generator. dev #18 starts routing the comparison side B through OpenAI &lt;code&gt;gpt-image-2&lt;/code&gt;.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 UI["Frontend generate"] --&gt; Backend["FastAPI /generate"]
 Backend --&gt; Gather["asyncio.gather()"]
 Gather --&gt; SideA["Side A &amp;lt;br/&amp;gt; Gemini 3.1 Flash"]
 Gather --&gt; SideB["Side B &amp;lt;br/&amp;gt; OpenAI gpt-image-2"]
 SideA --&gt; CompareUI["Frontend a/b keyboard compare"]
 SideB --&gt; CompareUI&lt;/pre&gt;&lt;p&gt;Key commits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Wire AsyncOpenAI client and OpenAI image-gen config&lt;/strong&gt; (&lt;code&gt;052d42f&lt;/code&gt;) — env vars, timeouts, retries in backend config.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shared image IO helper + OpenAI image service&lt;/strong&gt; (&lt;code&gt;1fb9b43&lt;/code&gt;) — adapter that normalizes Gemini and OpenAI responses into a common shape.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refactor 5-tone fields into side A/B semantics&lt;/strong&gt; (&lt;code&gt;d91067e&lt;/code&gt;, &lt;code&gt;ec38fa8&lt;/code&gt;) — &lt;code&gt;tone3&lt;/code&gt;, &lt;code&gt;tone5&lt;/code&gt; → &lt;code&gt;side_a&lt;/code&gt;, &lt;code&gt;side_b&lt;/code&gt;. The label stops describing tone variants and starts describing comparison sides.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shield gather from cancellation, drop unsupported quality param&lt;/strong&gt; (&lt;code&gt;8759a78&lt;/code&gt;) — &lt;code&gt;asyncio.gather&lt;/code&gt; cancels sibling tasks if any one raises. To keep both sides alive, shield with &lt;code&gt;return_exceptions=True&lt;/code&gt; and handle separately.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Two corner cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Aspect ratio mapping&lt;/strong&gt; — gpt-image-2 only supports &lt;code&gt;1024x1024&lt;/code&gt;, &lt;code&gt;1024x1792&lt;/code&gt;, &lt;code&gt;1792x1024&lt;/code&gt; (&lt;code&gt;97f7204&lt;/code&gt;). Map arbitrary UI ratios to the nearest supported size.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Surface side-B failures&lt;/strong&gt; (&lt;code&gt;7d31f62&lt;/code&gt;) — even on the &amp;ldquo;comparison side,&amp;rdquo; failures must be visible. Quietly missing data confuses evaluators.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="drop-injection-modes-introduce-a-modelproduct-library"&gt;Drop injection modes; introduce a model/product library
&lt;/h2&gt;&lt;p&gt;Through dev #17 there was a &amp;ldquo;tone injection mode&amp;rdquo; abstraction. Five tones × user-uploaded models × options spread out across the UI, and learning cost was high. dev #18 swaps it out — &lt;strong&gt;model tab and product tab, two tabs&lt;/strong&gt;.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 Lib["LibraryTab"] --&gt; ModelTab["Model (people photos)"]
 Lib --&gt; ProductTab["Product (object photos)"]
 ModelTab --&gt; ModelUpload["Direct upload"]
 ModelTab --&gt; ModelGen["Regenerate as ID-photo via Gemini"]
 ProductTab --&gt; ProductUpload["Upload + auto preprocess"]
 ProductUpload --&gt; AutoPick["Auto-pick on ready"]
 ModelGen --&gt; Generate["generate call"]
 ProductUpload --&gt; Generate&lt;/pre&gt;&lt;p&gt;Sequence:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Per-user asset library&lt;/strong&gt; (&lt;code&gt;b933191&lt;/code&gt;) — uploads stick to the user account; reusable across tones.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Replace injection-mode pills with model/product tabs&lt;/strong&gt; (&lt;code&gt;1450767&lt;/code&gt;) — UI simplifies. The &amp;ldquo;which mode&amp;rdquo; decision is gone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Regenerate uploaded models as ID-photos&lt;/strong&gt; (&lt;code&gt;db64b05&lt;/code&gt;) — clean up uploaded portraits via Gemini for a consistent model slot.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Role-aware prompt directives&lt;/strong&gt; (&lt;code&gt;ffb8ccf&lt;/code&gt;) — when model/product references go into the prompt, their roles are explicit: &amp;ldquo;this person as the model,&amp;rdquo; &amp;ldquo;this object as the product.&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Product preprocess + auto-pick on ready&lt;/strong&gt; (&lt;code&gt;69db8c2&lt;/code&gt;) — upload → background preprocess → auto-activate. One fewer click for the user.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Surface processing state + toast&lt;/strong&gt; (&lt;code&gt;f3ff587&lt;/code&gt;) — processing assets show a distinct state. No silent waits.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There was a back-and-forth in the middle. &lt;strong&gt;Auto model injection turned off, then back on&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bdf0aae&lt;/code&gt; — drop auto model injection, direct upload only (also fixes label wrap)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;394f91f&lt;/code&gt; — restore auto model injection, also accept generated-image drops&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Direct upload only meant users had to drop in models one at a time, which created friction. Auto-injection won as default; direct upload stayed as an option.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="tone-pool-curation-0428--0429--0504"&gt;Tone pool curation: 0428 → 0429 → 0504
&lt;/h2&gt;&lt;p&gt;Eighty percent of generation quality lives in the tone reference pool. Too varied → results scatter; too narrow → results all look alike.&lt;/p&gt;
&lt;p&gt;This cycle&amp;rsquo;s curation work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Swap model_image_ref to 0428 model selection pool&lt;/strong&gt; (&lt;code&gt;c1e5d39&lt;/code&gt;) — the 0428 set has more consistent lighting; promoted to main model pool.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Two-category tones + person-aware model slot&lt;/strong&gt; (&lt;code&gt;cb3a260&lt;/code&gt;) — split tones into two categories (natural/film, studio/clean), and only enable the model slot when the tone implies a person.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scope auto-pick to curated 0429 subfolders&lt;/strong&gt; (&lt;code&gt;27d335d&lt;/code&gt;) — when auto-picking a tone, only the 0429 curated set is in play. Cuts noise.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rewrite slug-named tone refs in generation_logs&lt;/strong&gt; (&lt;code&gt;76a1a64&lt;/code&gt;) — when the S3 corpus path naming changed, old logs needed remapping.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reseed a(natural,film) from 0429 to 0504&lt;/strong&gt; (&lt;code&gt;c43214e&lt;/code&gt;, the latest commit) — refresh the most-used tone category to the newest set.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A &lt;code&gt;scripts/&lt;/code&gt; directory now records the S3 corpus swap utilities (&lt;code&gt;f169dd4&lt;/code&gt;) so the next curation cycle can reuse them.&lt;/p&gt;
&lt;p&gt;A one-line &lt;code&gt;nginx&lt;/code&gt; fix (&lt;code&gt;9f252ff&lt;/code&gt;) had outsized impact. Backend timeouts and the nginx &lt;code&gt;/api/&lt;/code&gt; timeout were misaligned, so when OpenAI was slow, nginx returned 502 first and triggered backend retries. Aligned both, plus disabled upstream retries.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="internal-vs-external-permission-tiers"&gt;Internal vs external: permission tiers
&lt;/h2&gt;&lt;p&gt;This cycle introduced a real &lt;strong&gt;internal user&lt;/strong&gt; concept (team only). Demo days and external beta meant some features should not be visible to the world.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 User["Logged-in user"] --&gt; Check{"is_internal?"}
 Check -- "yes" --&gt; Internal["Internal features visible"]
 Check -- "no" --&gt; External["External (default)"]
 Internal --&gt; ToneLock["Tone refs pin &amp;lt;br/&amp;gt; lock across generations"]
 Internal --&gt; Admin["S3 image manager &amp;lt;br/&amp;gt; tone/model/product curation"]
 External --&gt; Generate["Standard generate flow"]&lt;/pre&gt;&lt;p&gt;Three PRs split the work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PR #16 — internal vs external user tiers + UI gating&lt;/strong&gt; (&lt;code&gt;f33e9d0&lt;/code&gt;) — &lt;code&gt;is_internal&lt;/code&gt; column on the user, internal-only components short-circuit to &lt;code&gt;&amp;lt;&amp;gt;&amp;lt;/&amp;gt;&lt;/code&gt; for everyone else.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PR #17 — internal-only tone-lock&lt;/strong&gt; (&lt;code&gt;199a405&lt;/code&gt;) — pin the same tone references across generations for clean A/B evaluation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PR #18 — internal-only S3 image manager&lt;/strong&gt; (&lt;code&gt;8096425&lt;/code&gt;) — manage tone/model/product corpus from the web UI instead of the S3 console.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;feat/admin-s3-manager&lt;/code&gt; branch needed two main merges (&lt;code&gt;9d5fa1e&lt;/code&gt;, &lt;code&gt;a35bf53&lt;/code&gt;). Other tracks landed mid-development and conflicts piled up — lesson: rebase the admin branch right after each major merge to main.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="cameralens-picker-polish"&gt;Camera/lens picker polish
&lt;/h2&gt;&lt;p&gt;Camera/lens selection UX got a one-cycle pass.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Commit&lt;/th&gt;
 &lt;th&gt;What&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;2439c98&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Show thumbnails in angle picker dropdown (preview before selection)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;4f615a7&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Zoom button on hover for reference image&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;5be9daa&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Rename to &amp;ldquo;Camera &amp;amp; Lens&amp;rdquo;, random default lens, model creator&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;b4aeed3&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Show None option explicitly + add None to LensPicker&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;228ff9f&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;LensPicker auto-closes after selection&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;024253e&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Angle/lens picker still clickable when default is None&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;bb13dd3&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Bottom bar polish — General + Edit on right + stronger active state&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;020c509&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Multi-line breathing room for generation prompt&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;8208a11&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Library tab + prompt area zoom + transparent overlay&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;349d142&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Re-roll filters in preview modal, drop dead labels&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Keyboard&lt;/td&gt;
 &lt;td&gt;A/B compare via arrow + &amp;lsquo;a&amp;rsquo;,&amp;lsquo;b&amp;rsquo; keys (&lt;code&gt;fad542e&lt;/code&gt;)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The keyboard compare ended up the most loved tweak. Toggling between two results with &amp;lsquo;a&amp;rsquo;/&amp;lsquo;b&amp;rsquo; rather than mousing back and forth doubled comparison velocity.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="insights"&gt;Insights
&lt;/h2&gt;&lt;p&gt;Going from #17 to #18, &lt;strong&gt;reducing abstraction was the move forward.&lt;/strong&gt; &amp;ldquo;Tone injection mode&amp;rdquo; was a 5-axis abstraction that imposed our code model on the user. The actual mental model is binary: &amp;ldquo;include a person, or include an object.&amp;rdquo; Two tabs match that, and learning cost dropped accordingly.&lt;/p&gt;
&lt;p&gt;Routing OpenAI alongside Gemini follows the same shape. Guessing which model is better from a single response is much worse than seeing both side-by-side and toggling with the keyboard. Details like &lt;code&gt;asyncio.gather&lt;/code&gt; shielding matter, but if you nail down what happens when one side dies, the pattern is reusable.&lt;/p&gt;
&lt;p&gt;The permission gate was a high-leverage tiny change. One &lt;code&gt;is_internal&lt;/code&gt; column + conditional UI rendering, and the internal-only S3 admin and tone-lock can sit in the main codebase without exposing them. Avoiding a separate admin app saved a lot.&lt;/p&gt;
&lt;p&gt;Next in dev #19: results from the gpt-image-2 quality A/B, a &amp;ldquo;group&amp;rdquo; concept in the model library (multiple people in one shot), and the conditions under which internal tone-lock could open up to external users.&lt;/p&gt;</description></item></channel></rss>