<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Spacy on ICE-ICE-BEAR-BLOG</title><link>https://ice-ice-bear.github.io/tags/spacy/</link><description>Recent content in Spacy on ICE-ICE-BEAR-BLOG</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Wed, 22 Apr 2026 00:00:00 +0900</lastBuildDate><atom:link href="https://ice-ice-bear.github.io/tags/spacy/index.xml" rel="self" type="application/rss+xml"/><item><title>hybrid-image-search-demo Dev Log #17 — HEX-Only Injection, Angle Category Split, Korean Prompt Extraction</title><link>https://ice-ice-bear.github.io/posts/2026-04-22-hybrid-search-dev17/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0900</pubDate><guid>https://ice-ice-bear.github.io/posts/2026-04-22-hybrid-search-dev17/</guid><description>&lt;img src="https://ice-ice-bear.github.io/" alt="Featured image of post hybrid-image-search-demo Dev Log #17 — HEX-Only Injection, Angle Category Split, Korean Prompt Extraction" /&gt;&lt;h2 id="overview"&gt;Overview
&lt;/h2&gt;&lt;p&gt;Sixteen commits, three threads: a new &lt;strong&gt;HEX-only injection mode&lt;/strong&gt; (the tone-image is dropped, only the hex palette is injected into the prompt), an &lt;strong&gt;angle picker split into three categories&lt;/strong&gt; with an inline UI, and a &lt;strong&gt;Korean-prompt attribute extractor&lt;/strong&gt; that stops the prod regression where &amp;ldquo;하늘을 달리는 남자&amp;rdquo; got a female model attached. A small OTLP tuning pass (batched spans, widened metric interval) wraps it up.&lt;/p&gt;
&lt;p&gt;Previous post: &lt;a class="link" href="https://ice-ice-bear.github.io/posts/2026-04-17-hybrid-search-dev16/" &gt;hybrid-image-search-demo Dev Log #16&lt;/a&gt;&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 U["User Prompt (KO)"] --&gt; Extract["Attribute Extractor&lt;br/&gt;spaCy + few-shot LLM"]
 Extract --&gt; Gender["gender"]
 Extract --&gt; Age["age"]
 Extract --&gt; Race["race"]
 Gender --&gt; Model["Model image injection"]
 Age --&gt; Model
 Race --&gt; Model
 U --&gt; Inject{Injection Mode}
 Inject --&gt;|off| P0["Plain prompt"]
 Inject --&gt;|auto| PT["Tone image + hex"]
 Inject --&gt;|hex_only| PH["Hex palette only"]
 Model --&gt; Prompt["Final prompt"]
 P0 --&gt; Prompt
 PT --&gt; Prompt
 PH --&gt; Prompt&lt;/pre&gt;&lt;h2 id="hex-only-injection-mode"&gt;HEX-Only Injection Mode
&lt;/h2&gt;&lt;p&gt;The tone-injection system has two axes: &lt;strong&gt;the tone reference image&lt;/strong&gt; (3- or 5-image pack, extracted to hex colors) and &lt;strong&gt;the prompt fragment&lt;/strong&gt; that frames the image inside the generation prompt. The existing default wired both — inject the image and the tone-direction text. The ask in this session was a third mode where only the hex palette goes into the prompt as color guidance, and the tone image path is skipped entirely.&lt;/p&gt;
&lt;p&gt;The design (&lt;code&gt;44d5bff&lt;/code&gt;, &lt;code&gt;08916cb&lt;/code&gt;) unified this as a three-way &lt;code&gt;injection_mode&lt;/code&gt; enum: &lt;code&gt;off&lt;/code&gt; / &lt;code&gt;auto&lt;/code&gt; / &lt;code&gt;hex_only&lt;/code&gt;. Plumbing it through was the bulk of the work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;refactor(prompt)&lt;/code&gt;: promote &lt;code&gt;hex_colors&lt;/code&gt; to an explicit param and add a hex-only prompt block (&lt;code&gt;0a16f4f&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(db)&lt;/code&gt;: thread &lt;code&gt;injection_mode&lt;/code&gt; through &lt;code&gt;log_generation&lt;/code&gt; and hydration (&lt;code&gt;e53be41&lt;/code&gt;) — necessary so the generation record can reconstruct the mode later for debugging.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(backend)&lt;/code&gt;: wire &lt;code&gt;injection_mode&lt;/code&gt; end-to-end off/auto/hex_only (&lt;code&gt;e6807e2&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Alembic migration &lt;code&gt;20260420_add_injection_mode.py&lt;/code&gt; adds the column.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(ui)&lt;/code&gt;: three-way toggle pill, with a11y tooltips explaining each mode (&lt;code&gt;5659fd3&lt;/code&gt;, &lt;code&gt;3b2cf22&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The UI polish (&lt;code&gt;51464e6&lt;/code&gt;) took a few iterations. The initial design was neutral-gray for inactive states, but &amp;ldquo;off&amp;rdquo; didn&amp;rsquo;t read as &lt;em&gt;disabled&lt;/em&gt; — users kept thinking it was still active. The fix: inactive pills turn red, the active pill yellow. Stronger contrast, the state legible at a glance.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;fix(ui)&lt;/code&gt; commit (&lt;code&gt;988ea37&lt;/code&gt;) covers the prompt-display helpers that were still showing the tone-direction text even in &lt;code&gt;hex_only&lt;/code&gt; mode — a dangling copy path. And &lt;code&gt;chore(gen)&lt;/code&gt; (&lt;code&gt;c419349&lt;/code&gt;) polished the telemetry labels so the three modes show up clearly in Grafana spans.&lt;/p&gt;
&lt;h2 id="angle-picker-three-categories-with-inline-ui"&gt;Angle Picker: Three Categories with Inline UI
&lt;/h2&gt;&lt;p&gt;Commit &lt;code&gt;61c5802&lt;/code&gt; splits angle selection from a flat list into three categories with an inline UI (presumably &amp;ldquo;general / beauty / product&amp;rdquo; or similar, based on the prior Lens picker pattern in #16). The structural motivation is the same as the lens picker expansion two logs back: flat lists over ~5 items become noise; grouping restores scanability.&lt;/p&gt;
&lt;p&gt;The subtle frontend concern was that the three-category split needed a deterministic display order — reflectiveness in the backend&amp;rsquo;s &lt;code&gt;angle_registry&lt;/code&gt; plus category metadata in the JSON schema. The component reads the schema once and renders sections; selection still emits a single &lt;code&gt;angle_id&lt;/code&gt; to the backend, so the API surface is unchanged.&lt;/p&gt;
&lt;h2 id="korean-prompt-attribute-extraction"&gt;Korean Prompt Attribute Extraction
&lt;/h2&gt;&lt;p&gt;The prod bug that kicked this off: a prompt &amp;ldquo;하늘을 달리는 남자&amp;rdquo; (a man running across the sky) produced a generation where the model-injected reference image was &lt;code&gt;Araya 05.png&lt;/code&gt;, which is labeled female in &lt;code&gt;data/model_labels.json&lt;/code&gt;. The LLM-driven attribute extractor was picking the wrong gender.&lt;/p&gt;
&lt;p&gt;The fix (&lt;code&gt;61e6c85&lt;/code&gt;) is a &lt;strong&gt;few-shot prompt&lt;/strong&gt; that enforces gender/age/race extraction with examples. Tightening the prompt schema rather than running a classifier keeps the solution simple — the decision made in-session was that a minor guardrail was enough, since the input space of Korean prompts is broad and any real classifier would need a labeled corpus.&lt;/p&gt;
&lt;p&gt;spaCy pinning (&lt;code&gt;9f2773b&lt;/code&gt;) is related — &lt;code&gt;en_core_web_sm&lt;/code&gt; was auto-upgrading in fresh venvs, and the prompt parser relies on specific token types. Pinning ensures reproducible parses.&lt;/p&gt;
&lt;h2 id="otlp-telemetry-tuning"&gt;OTLP Telemetry Tuning
&lt;/h2&gt;&lt;p&gt;Two small but load-bearing changes (&lt;code&gt;02c0c6c&lt;/code&gt;): &lt;strong&gt;batch spans&lt;/strong&gt; instead of sending per-span (one of the OpenTelemetry defaults that absolutely needs overriding under any real traffic), and &lt;strong&gt;widen the metric interval&lt;/strong&gt; so Grafana Cloud free-plan ingestion stays well under limits. The trial expired — the dashboards need to fit in free.&lt;/p&gt;
&lt;h2 id="commit-log"&gt;Commit Log
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Message&lt;/th&gt;
 &lt;th&gt;Changes&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;docs(spec): HEX-only tone injection mode design&lt;/td&gt;
 &lt;td&gt;design&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;docs(plan): HEX-only tone injection implementation plan&lt;/td&gt;
 &lt;td&gt;plan&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;refactor(prompt): promote hex_colors to explicit param, add hex-only block&lt;/td&gt;
 &lt;td&gt;prompt builder&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat(angle): split angle selection into 3 categories with inline UI&lt;/td&gt;
 &lt;td&gt;angle picker&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;chore(deps): pin en_core_web_sm so venv rebuilds include spaCy model&lt;/td&gt;
 &lt;td&gt;reproducibility&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat(db): thread injection_mode through log_generation and hydration&lt;/td&gt;
 &lt;td&gt;DB + ORM&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat(backend): wire injection_mode end-to-end (off/auto/hex_only)&lt;/td&gt;
 &lt;td&gt;backend wiring&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix(deploy): restart backend and frontend via pm2&lt;/td&gt;
 &lt;td&gt;deploy hotfix&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat(ui): 3-way injection mode toggle (off/auto/hex_only)&lt;/td&gt;
 &lt;td&gt;UI&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;chore(ui): polish injection mode pill a11y and tooltips&lt;/td&gt;
 &lt;td&gt;a11y&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix(ui): respect hex_only mode in prompt-display helpers&lt;/td&gt;
 &lt;td&gt;UI sync&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;style(ui): make all inactive injection pills red for stronger active signal&lt;/td&gt;
 &lt;td&gt;visual contrast&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix(generation): extract gender/age/race reliably from Korean prompts&lt;/td&gt;
 &lt;td&gt;parser fix&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix(telemetry): batch spans and widen metric interval&lt;/td&gt;
 &lt;td&gt;OTLP tuning&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="insights"&gt;Insights
&lt;/h2&gt;&lt;p&gt;Two threads share the same lesson: &lt;strong&gt;explicit modes beat implicit fallbacks.&lt;/strong&gt; The &lt;code&gt;injection_mode&lt;/code&gt; enum is strictly better than the prior &amp;ldquo;the flag defaults affect two things simultaneously&amp;rdquo; design, because each code path is now legible at the call site — no need to trace through eight booleans. Similarly, the Korean prompt extractor used to rely on an LLM&amp;rsquo;s default behavior, which worked &lt;em&gt;most&lt;/em&gt; of the time until it didn&amp;rsquo;t; a few-shot prompt is still LLM-based, but now the decision is visible in the prompt itself. Visual contrast follows the same principle: the moment an &amp;ldquo;off&amp;rdquo; toggle looks neutral, users stop reading it as off. Next session&amp;rsquo;s likely focus: the auto-fill token expansion request from session 4, which needs a thoughtful UI for editing 3 or 5 tone refs at once instead of one-at-a-time.&lt;/p&gt;</description></item></channel></rss>