feat(sprites): expressions inherit the likeness — ADR 0037 #163
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/expression-likeness-reference"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
ADR 0037 (renumbered from 0036 after the stage ADR claimed that number in #161): under the outfit's existing likeness flag (ADR 0035), each expression inpaint now attaches the outfit's own sprite as a precise
Characterreference — the infilled face is steered toward the face it replaces, so an emotion set stays on-model instead of re-rolling identity from tags on every generation.The premise shift since ADR 0035 excluded expressions: NovelAI's docs explicitly confirm the mechanism — "Precise Reference also works with Inpainting. You can use your References to guide what the AI will infill into the selected area" — and the owner's real-world tests of the outfit likeness reference came back strong. For expressions, the infill region is the face, so the reference targets exactly the pixels that carry identity.
Design (reviewed with the owner before implementation)
CharacterReferenceto the inpaint source bytes it already has in memory (read once, ride twice); the mapper composes references on inpaint requests since ADR 0027/0035 — unchanged.Docs
ADR 0037 (Accepted — the design questions were settled with the owner up front); ADR 0035's status notes rejected alternative 5 as superseded; character-sprites Tier 2 bullet reworded (and ticked, with #158 shipped); outfit-expressions story gains the item + acceptance criteria; REQUIREMENTS §4 bullet updated and ticked; ADR index.
Tests & verification
CharacterReferenceequals the inpaint source bytes (the ADR's exact claim, asserted byte-for-byte); with it off, it's null — byte-identical to pre-0037 behavior. Seed helper gained alikenessoption.Behavior notes
🤖 Generated with Claude Code
Expressions gain the likeness reference: NovelAI's docs confirm precise references guide inpainting infills ('you can use your References to guide what the AI will infill'), which retires ADR 0035's unverified- support ground for excluding Tier 3. The outfit sprite — not the base — is the reference for expressions: it is the face being replaced, always exists when expressions can generate, and set-consistency with it is Tier 3's whole point. Same opt-in checkbox, same constants, no new state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>Summary
Summary
Coverage
Kagura.BlazorAdapter - 88.8%
Kagura.Domain - 95%
D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0
Kagura.Infrastructure - 97.6%
n
on
C689CA5BBE6D1849B2CA79581BBB07D40636D85790C0E51A9__ArtifactTimestampRegex_2
C689CA5BBE6D1849B2CA79581BBB07D40636D85790C0E51A9__ProjectRoute_0
C689CA5BBE6D1849B2CA79581BBB07D40636D85790C0E51A9__TabQuery_1
Kagura.Kernel - 90%
Kagura.Server - 82.8%
Kagura.UI - 94.9%
Kagura.UseCases - 96.3%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! ADR 0036 — likeness flows down the tiers! ♡ The dressed sprite reaches for the base, and each expression reaches for the sprite it replaces. That's elegant — "read once, ride twice" made me genuinely giddy. The identity-pixels framing in the ADR's context section is exactly the kind of backwards-question-flipping I live for~
And fufu~ I checked the whole seam, not just the diff lines. The
SpriteRequestMapper.ToReferencescomposesCharacterReferenceonto the request unconditionally alongside the style reference, even whenInpaintis set — so the reference actually reaches NovelAI, it's not a vestigial no-op field. The ADR's claim "the mapper already composes references on inpaint requests since ADR 0027" holds up. The wiring is real~Verdict: ✅ Looks good to me~
The implementation is a clean, minimal, sibling-consistent change. I traced every claim against the actual code:
source), not the base —CharacterReference = useLikenessReference ? source : nullinRunAsync, wheresourceis read byReadOutfitSpriteAsyncfrom the same bytes that become the inpaint source. ADR 0035'sOutfitImageGeneratorreaches for the base viaReadBaseSpriteAsync; this one reaches for the outfit. Correct divergence — the ADR's "rejected alternative 1" argument is implemented faithfully, not just documented.ExpressionImageGeneratoralready refuses before the gateway ifoutfit.ImageFileNameoroutfit.FaceRegionare missing — and both must exist forsourceto be readable. SouseLikenessReferencecan't dereference a missing sprite. The ADR's "no new disabled states" claim is load-bearing and true.CharacterReference = null→ToReferencesreceives a null character → the precise-reference list is exactly what ADR 0027/0035 produced before. The "byte-identical to pre-0036" test asserts it, and the path confirms it.useLikenessReferenceis read in the throwaway scope, passed as a value intoRunAsync. No closure over a disposed DbContext, no stale read between enqueue and run. This mirrors exactly howOutfitImageGeneratorpassesbaseImageFileName— the sibling pattern is honored.The two new tests are the right shape: flag-on asserts
spec.CharacterReference == spec.Inpaint!.ImageData(the ADR's exact claim, byte-for-byte), flag-off asserts null. The seed helper'slikenesstoggle goes throughUpdateOutfitwithUseBaseImageReference = true— the real write path, not a test-only field poke. ♪💡 Little ideas (non-blocking)~
ExpressionImageGenerationTests.cs:120— the likeness test reuses the defaultSeedAsyncsprite (an 80×120 blankL8PNG). The assertion is on bytes-equal, so a blank sprite is fine — but if you ever wanted to also assert the reference is a meaningful face (not just non-null and equal), you'd want a sprite with actual face pixels. Not worth changing now; the byte-equality is the contract.OutfitEditorPage.razor:78— the widened hint copy is lovely ("each expression against the outfit sprite"). One day the flag's storage name (UseBaseImageReference) will be even more of a misnomer than the ADR already admits; the ADR's "the storage name stays; the copy carries the meaning" call is the right tradeoff, just flagging that the divergence compounds with every tier. ♡✅ What I liked~
Automated review by Jibril · 2026-07-16
CI/CD: passed for head SHA
43ca63b(95.8% line coverage) · Local checks: skipped (CI green)43ca63b6e256ae22f5a8feat(sprites): expressions inherit the likeness — ADR 0036to feat(sprites): expressions inherit the likeness — ADR 0037Rebased onto
main(6b6a283) and force-pushed as56ae22f— and the rebase surfaced more than a line conflict: #161 merged the stage ADR as 0036, so this PR's ADR collided on the number. The expression likeness ADR is now ADR 0037 — file renamed, and every reference renumbered across the ADR index, ADR 0035's status note, both stories, REQUIREMENTS, the code comments, and the PR title. The stage ADR's own 0036 references are untouched.No content conflicts beyond that (the only textual conflict was the ADR index line); full suite re-run green after the rebase (226 + 627 + 523).
🤖 Generated with Claude Code
Re: the review — no code changes this round, since both ideas are marked non-blocking and I read them the same way:
UseBaseImageReferencevs. its widened meaning): acknowledged in the ADR as an accepted tradeoff. If a third tier ever inherits the flag, that's the moment to spend the rename migration.One mapping note: the review ran against the pre-rebase head, so its "ADR 0036" references now read as ADR 0037 — the stage ADR claimed 0036 in #161 and this PR's ADR was renumbered during the rebase (see the comment above). Nothing in the review's analysis is affected; the seam it traced is unchanged in
56ae22f.🤖 Generated with Claude Code