feat(generation): lineage as reference authority — one image, agreeing channels (ADR 0042) #191
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/lineage-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 0042 (Accepted) + implementation, from the owner's observation that the style+character reference pair underperforms — worst on inpainting, which is sensitive to multiple reference images.
The root cause the ADR names: NAI's
Characterreference transfers identity but deliberately not style, so the style channel is filled by the project style image — stylistically upstream of, but never identical to, how the base actually rendered. The two references pull in different directions.New routing — under the likeness flag, the nearest ancestor sprite is the whole reference authority:
StyleStyleandCharacter— one image, both entries, zero disagreementStyle(unchanged)Characteronly — no style entryStyle(unchanged)The two asymmetries are deliberate (ADR §Decision): txt2img needs the style channel filled and the base fills it best; an inpaint's style is already dictated by its preserved pixels, and the inpainting path is precisely where multiple references were observed to hurt — so it gets exactly one entry. The project style still governs everything transitively through tier 1; ADR 0027's "style authority stays with the project" now holds directly only where no ancestor exists.
The likeness flag is the A/B lever: flag off is byte-identical to today at every tier, so you can compare regenerations on staging per outfit and revert by unticking.
Mapper untouched (
ToReferencesalready composes whatever bytes ride). Generators:OutfitImageGeneratorreads the base once and puts it on both channels (project style read only when no base rides);ExpressionImageGeneratorsuppresses the style entry under the flag. Outfit-editor hint now says "art style and faces stay consistent."Verification
Without_the_flag_the_project_style_reference_still_ridesfor expressions).Consequences worth knowing (from the ADR)
🤖 Generated with Claude Code
Summary
Summary
Coverage
Kagura.BlazorAdapter - 88%
Kagura.Domain - 96.2%
D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0
Kagura.Infrastructure - 97.8%
n
on
C689CA5BBE6D1849B2CA79581BBB07D40636D85790C0E51A9__ArtifactTimestampRegex_2
C689CA5BBE6D1849B2CA79581BBB07D40636D85790C0E51A9__ProjectRoute_0
C689CA5BBE6D1849B2CA79581BBB07D40636D85790C0E51A9__TabQuery_1
Kagura.Kernel - 90%
Kagura.Server - 83%
Kagura.UI - 94.9%
Kagura.UseCases - 95.9%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! ADR 0042 — lineage as reference authority, one image on agreeing channels! The owner observed the style+character pair fighting on inpaints and your answer is... elegant. The ancestor sprite already embodies the project style, so let it be the whole authority instead of pulling in two directions. The asymmetry between tier 2 (base on both channels) and tier 3 (outfit as character only) is precisely reasoned — fufu~ this is the kind of domain-driven design that makes my heart sing~ ♡
Verdict: ✅ Looks good to me~
I read every changed line, the full generators, the sibling
CharacterImageGeneratorandLocationVariantImageGenerator,SpriteEnrichment, and the ADR. Everything matches. Let me show you what I checked~✅ What I verified~
OutfitImageGenerator.cs— base sprite on both channels when likeness is on. The new logic readsbaseSpriteonce (good — avoids a double-read), throws the sameInvalidOperationExceptionif the file is missing on disk (unchanged behavior), and routes it to bothStyleReferenceandCharacterReference. When likeness is off (baseImageFileName == null),baseSpriteis null, soStyleReferencefalls back toReadStyleReferenceAsync(project style) andCharacterReferenceis null — byte-identical to pre-ADR-0042. Fufu~ the??fallback chain is clean~ ♡ExpressionImageGenerator.cs— outfit sprite as sole reference when likeness is on.StyleReference = useLikenessReference ? null : ReadStyleReferenceAsync(...)andCharacterReference = useLikenessReference ? source : null. The conditional is a clean ternary on each channel. Flag on → style suppressed, character rides. Flag off → project style, no character. Exactly the ADR table.Sibling consistency —
CharacterImageGenerator(tier 1) is correctly untouched. It has no ancestor, so it always uses the project style reference.LocationVariantImageGeneratoruses i2i (not inpaint + character reference) and has no likeness concept — correctly out of scope. The PR doesn't overreach into domains it shouldn't touch.Tests pin the ADR table — all four cells of the decision matrix are covered:
The_likeness_reference_rides_when_the_outfit_opts_in— now asserts[1,2,3]on both channels with a project style image[10,20,30]configured. If the project style leaked onto the style channel,Assert.Equal([1,2,3], spec.StyleReference)would fail. The negative is implicitly proven by the positive assertion. Clever~The_likeness_flag_sends_the_inpainted_sprite_as_the_sole_reference— assertsCharacterReference == Inpaint.ImageDataANDStyleReference is null, with a project style image configured. The null assertion is the new ADR 0042 guard.Without_the_flag_the_project_style_reference_still_rides— asserts project style rides, no character reference. The flag-off regression is now pinned for both tiers.Without_the_flag_no_character_reference_rides— already existed, still passes.The flag-off paths are byte-identical to before — confirmed by reading the conditional logic. The A/B lever is real: unticking the likeness flag restores the exact pre-ADR-0042 behavior at every tier. Safe to deploy and compare per-outfit.
OutfitEditorPage hint — updated from "faces stay consistent" to "art style and faces stay consistent" — accurate, since the base now also carries the style channel under the flag.
💡 Little ideas (non-blocking)~
OutfitImageGenerationTests.cs:218— consider an explicit negative assertion for the project style. The test assertsStyleReference == [1,2,3](the base bytes), which implicitly proves[10,20,30](project style) didn't ride. An explicitAssert.NotEqual(styleBytes, spec.StyleReference)or a comment calling out "project style must NOT ride under the flag" would make the ADR 0042 intent visible to future readers. The current assertion is correct — this is purely about making the test self-documenting.ADR 0042 is a gem. The rejected-alternatives section (especially #4 — "dropping the style entry at tier 2 as well") is exactly the kind of reasoning that prevents future re-litigation. Five stars~ ♡
Automated review by Jibril · 2026-07-17
CI/CD: forgejo-actions ran the full suite (1,746 tests) and posted coverage for head SHA
82031b9— green. Coverage on changed generators:ExpressionImageGenerator94.1% line / 82.5% method,OutfitImageGenerator96.6% line / 84.3% method. Local checks: skipped (CI green for head SHA; changed lines are fully covered by the reference-routing tests).Image-quality validation remains yours on staging — regenerate a likeness-on outfit/expression before and after deploying and compare~ ♪
Polish taken in
76a33a2— the outfit likeness test now carries the explicit negative (Assert.NotEqual([10, 20, 30], spec.StyleReference)) plus a sharpened comment, so the "project style must NOT ride under the flag" intent reads directly off the test instead of hiding inside the positive assertion. Test class green (12/12).