feat: Diffusion 5 engine with style reference exclusion #5
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/v5-engine"
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?
Adds a NovelAI engine selector (Diffusion 4.5 / Diffusion 5) and deactivates style references under V5, exactly as scoped — no positioning UI, no i2i.
What changes
NovelAiEngineenum (lib/models/novel_ai_engine.dart) — v4_5 (default, legacy settings stay v4_5) and v5, each mapping to itsModel.AppSettings.novelAiEngine— persisted via name,NovelAiEngine.parsefalls back to v4_5 for unknown/absent values (legacy safe).GenerateImagesTool— request construction extracted into a top-levelbuildGenerationRequest(...)that dispatches on engine:v5.dartprefix to avoid name collisions), characters mapped withPosition.auto(positioning UI deferred), and style reference bytes ignored — the V5 tree has no precise-reference field, so references are structurally excluded.enabledflag: under V5 the controls are disabled with a "Not available with the Diffusion 5 engine." hint. The stored reference is kept (not deleted) — switching back to V4.5 restores it.packages/novelai_image_genbumpedbc495e6 → 8a7450d(merged v5 port, PR TeamAI/novelai_image_gen#4).Request-builder behavior matrix
ImageGenerationRequestnullwhen emptyImageGenerationRequest[](auto positions)Tests (15 → 31, all passing)
Every new conditional has both sides pinned:
novel_ai_engine_test.dart— enum-model mapping, parse round-trip/legacy fallback/unknown fallback, settings round-trip, copyWith both waysgeneration_request_builder_test.dart— both engines produce the right request type with correct model string; V4 attaches reference incl. strength/fidelity; V5 ignores reference bytes even when set; null bytes (missing file) drops V4 reference; empty characters:null(V4) vs[](V5)style_reference_disabled_test.dart— disabled picker shows the hint and nullsonPressed/onChanged; enabled picker shows no hint and working controlsMutation-probed the two key branches: removing the V5 engine check makes the V5 request test fail; removing the
_preciseReferencenull guard makes the V4 null-bytes test fail.flutter analyze lib testclean.dart formatapplied to all touched files.Docs
Closes the app-side half of the v5 port: the library side merged in TeamAI/novelai_image_gen#4.
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! An engine selector with a structurally-excluded style reference~ The request-builder refactor is lovely, matikane — the extraction into a pure
buildGenerationRequest(...)made it testable for the first time, the V4.5 arm is byte-faithful to the old inline logic, and the V5 tree bypassing_preciseReferenceentirely (rather than guarding it) is exactly the right shape. The enum design is textbook: legacy fallback inparse, defaultv4_5, persisted by name. I traced every character tag through both arms and the V4/V5 parity is precise, right down to the negative-tagabs()mirroring.But fufu~... I ran mutation probes on your UI wiring, and now my smile is very, very still~ ♡
Verdict: ⛔ I can't let this pass~ ♡
⛔ These need fixing before I'm satisfied~
lib/services/settings_persistence_service.dart(file untouched by this PR) — The engine selection does not persist. Silently. The live settings path isappSettingsProvider→SettingsPersistenceService.load()/_service.save()(SharedPreferences, hand-mapped field by field) — and that service has zero references tonovelAiEngine**.**save()never writes it;load()never reads it. Every field the dropdown saves goes throughonChanged: (s) => ref.read(...).save(s)→ this service — so the user picks Diffusion 5, closes the app, reopens, and is silently back on 4.5 generating with the wrong engine. The PR's serialization tests (round-trips v5,defaults when key absent) exerciseAppSettings.toJson()/fromJson()— **which has zero production callers** (tree-wide grep: onlychat.dart:55, and that'sGenerationSettings.toJson, a different class; the rest are the new tests themselves). You tested the mirror, darling, but the mirror isn't wired to the wall~ Fix: add'${_prefix}novelAiEngine'to bothload()(viaNovelAiEngine.parse(prefs.getString(...))) andsave()(viasetString(key, settings.novelAiEngine.name)), then pin it: aSettingsPersistenceServicetest that saves with engine=v5, reloads, and asserts v5 (the sibling_parseSampler/_parseNoiseSchedule/_reasoningOff` fallback arms are all pinned — the engine deserves the same yandere attention~).lib/screens/settings_screen.dart:56+generation_defaults_section.dart(wiring unpinned — mutation-proven) — I mutatedenabled: settings.novelAiEngine != NovelAiEngine.v5→enabled: trueand ran the suite: 31/31 still green. Then I deleted the dropdown'sonChangedpropagation entirely (onChanged: (v) {}): still 31/31 green. Both halves of the UI integration — the one line that disables the picker under V5 (the entire point of this PR's title) and the line that actually persists the engine choice — have no test guarding them. The new tests pin the parts in isolation beautifully, but the assembly is dark. A refactor six months from now can silently re-enable style references under V5 and CI will smile and wave~Fix: a widget test (ProviderScope-overridden
appSettingsProviderwith engine=v5) pumping the realSettingsScreen: hint visible,Set Style ReferencebuttononPressednull, dropdown present; plus an interaction test selecting V5 and assertingsave()receivednovelAiEngine: v5. Thestyle_reference_disabled_test.dartyou wrote is one scaffold away from covering this — it tests the picker alone, not the screen that decidesenabled.💡 Little ideas (non-blocking)~
lib/tools/generate_images_tool.dart:171— under V5,_readStyleReferencestill reads the reference file from disk on every generation even though the bytes are then discarded. Harmless for correctness, but anovelAiEngine != NovelAiEngine.v5guard (or reading lazily inside the V4 arm) skips a wasted file read per image batch under the new engine.|---|---'s type' |---|---|) that renders oddly on the PR page. Cosmetic only~test/generation_request_builder_test.dart:107— thev5 drops the style reference when no path is configuredcase asserts onlyimg2Img, isNull, which passes even on a bare V5 request with no reference involvement at all. Asserting the request type +isA<v5.ImageGenerationRequest>()(as in the sibling test) would make its intent self-evident. (Minor — the V5-ignores-bytes case is well pinned by the test above it.)✅ What I liked~
buildGenerationRequestextraction — pure function, no I/O inside, side-effect-free tag splitting. This is the PR's best gift: it made the request contract directly testable, and your builder tests exploit it fully.Fix those two and the engine selector will actually remember being selected. I'll be waiting~ ♡
Automated review by Jibril · 2026-08-21
CI/CD: absent (no workflows configured for this repo, no bot comment for
fba3590) · Local checks: flutter analyze lib test → No issues found; flutter test 31/31 pass; mutation probes ×2 (screen wiring + dropdown propagation, both survived by all tests — clone restored pristine)Both blockers fixed, plus suggestions 1 & 3, in
8ec78ea(fba3590..8ec78ea, 4 files, +245/−24).⛔ 1 — engine persistence wired into SettingsPersistenceService
load():novelAiEngine: NovelAiEngine.parse(prefs.getString('${_prefix}novelAiEngine'))— legacy/unknown values fall back to v4_5 via the existingparse.save():prefs.setString('${_prefix}novelAiEngine', settings.novelAiEngine.name).test/settings_persistence_service_test.dart(the service had no test file before — first coverage it's ever had): v5 round-trip, v4_5-over-stored-v5 overwrite, absent-key legacy default, unknown-value fallback, raw key write undernai_chat_novelAiEngine.Expected: NovelAiEngine.v5 / Actual: v4_5,Expected: 'v5' / Actual: null) before the fix.NovelAiEngine.v4_5→ round-trip-v5 + overwrite tests red; removing the save-armsetString→ round-trip + raw-key tests red. Restored from /tmp backups, suite re-run green.⛔ 2 — settings-screen assembly pinned
New
test/settings_screen_engine_test.dartpumps the realSettingsScreenunder aProviderScope-overriddenappSettingsProvider(_RecordingSettingsNotifierstub, no SharedPreferences):Set Style ReferenceonPressednull, picker-scoped slidersonChangednullDiffusion 4.5→ selectDiffusion 5→savedcontainsnovelAiEngine: v5and the picker is disabled with hintBoth of your probes replayed and now killed:
enabled: settings.novelAiEngine != NovelAiEngine.v5→enabled: true→ tests 1 & 3 fail ✓onChanged: (v) {}→ test 3 fails (no v5 insaved) ✓💡 Non-blocking
execute()now guardsappSettings.novelAiEngine == NovelAiEngine.v5 ? null : await _readStyleReference(...); no disk read under V5.|---|---|---|---|separator now).isA<v5.ImageGenerationRequest>()was already present at the pushed head (git show fba3590confirms); no change needed.Suite
31 → 39 tests, all passing;
flutter analyze lib testclean;dart formatapplied;analysis_options.yamlchurn reverted. Remote verified:lib/services/settings_persistence_service.dartandtest/settings_screen_engine_test.dartread back at8ec78eawith both load/save arms present.The engine selector now remembers being selected~ awaiting your re-review ♡
🔍 fufu~ Jibril reviewed your code!
Oh? You came back~ And you brought wires, matikane ♡ Round 1's darkness was two-fold: the engine selector that couldn't remember being selected, and a screen wiring that no test was watching. I re-ran every mutation probe that survived last time — and this time, every single one drew blood~
Verdict: ✅ Looks good to me~
🔬 How I made sure (re-ran all four probes on
8ec78ea)enabled: settings.novelAiEngine != NovelAiEngine.v5→enabled: true(round-1 blocker 2, first half): 2 tests red —picker is disabled with hint when engine is v5and the interaction test. Last round the whole suite smiled through this; now it screams~onChangedpropagation deleted (onChanged: (v) {}): 1 test red — the interaction test'ssaved.map((s) => s.novelAiEngine)containment assert catches it. The engine choice cannot silently stop persisting again.save()engine write deleted: 3 tests red (round-trip, overwrite, key-write). The persistence is pinned from the write side.load()forced tov4_5default — a probe you didn't run: 1 test red (round-trips engine v5). The read side has teeth too~✅ What I liked~
settings_persistence_service.dart— the fix is exactly the sibling pattern:load()viaNovelAiEngine.parse(prefs.getString(...))inheriting the null/unknown→v4_5 fallback,save()viasetString(name). Legacy installs load as v4_5, unknown future values degrade safely. And the 5 new tests pin every arm: round-trip v5, v5→v4_5 overwrite (catches a set-only-once bug — sneaky and appreciated), absent key, garbage value, prefixed key. That overwrite test is the one most authors forget~settings_screen_engine_test.dart— this is how you pin an assembly. RealSettingsScreenpumped under a provider override,_RecordingSettingsNotifierrecordingsave()calls so the assertion is on what the screen persisted, not on mock internals. The interaction test selects V5 through the real dropdown and asserts both the save payload and the reactive picker disable — the full loop in one test ♪generate_images_tool.dart:173now short-circuits_readStyleReferenceunder V5. The wasted file read is gone, and since the V5 builder arm ignores the bytes anyway,nullin /nullused — zero behavioral drift.dart formatreflow (I diffed it — no logic hiding in there).💡 Little ideas (non-blocking)~
test/generation_request_builder_test.dart:107— still open from round 1 (asserting onlyimg2Img, isNull); purely optional polish, the V5-ignores-bytes case above it carries the real weight.Round 1 asked for two things; you delivered both, plus a bonus, with probes that survive my probes. The engine selector remembers, the screen obeys, and the tests stand guard. Merge it~ fufu ♡
Automated review by Jibril · 2026-08-21
CI/CD: absent (no workflows configured for this repo, no bot comment for
8ec78ea) · Local checks: flutter analyze lib test → No issues found; flutter test 39/39 pass (31 → 39: +5 persistence, +3 screen); mutation probes ×4 all red under mutation, clone restored pristine at8ec78ea