Add event scenes and per-variation scene generation #1
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feature/event-scenes"
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?
Summary
Adds Event Scenes as a third top-level asset type alongside Characters and Locations. A scene acts as a reusable story-CG stage with scene-level tags, optional location defaults, style references, and child variations. Each variation is an independently generated widescreen CG with its own cast, editable tabbed tag pools, optional seed, and generated image.
This PR implements the revised v2 Event Scenes requirements from
docs/srs-event-scenes.md, including the shift from scene-level cast to per-variation cast, neutral character imports, editable per-character tag tabs, and a single add-character modal with live preview.What changed
Event-scene domain model and persistence
SceneDefinitionVariationDefinitionImportedCharacterImportedLocationCharacterTabStateISceneStore/SceneStorefor_scenesstorage under the configured save root.scene.jsonplus generated variation images undervariations/.imported/{importId}/folders, preserving snapshot semantics even if source characters are later edited or removed.Scene overview UI
SceneOverviewViewModel/SceneOverviewViewfor scene-level authoring:Location & Scenetag pool while preserving scene/user-authored tags.Variation editor UI
VariationEditorViewModel/VariationEditorViewfor per-variation editing:Location & Scenetab plus one tab per imported characterAddCharacterDialog/AddCharacterDialogViewModelthat selects character + outfit together and shows a live preview thumbnail.Generation pipeline
GenerateEventScenejob type.SceneJobBuilderandSceneReferenceBuilderto build event-scene jobs from the persisted scene/variation state.Export and settings
SceneDefaultsto global settings with widescreen resolution/default tags for Event Scenes._scenes/{SceneName}/{VariationName}.png.ImageGen submodule update
external/NovelAI.ImageGenfrom0e2b164to1bb4d12.InHeatandAhegaoemotion support plus augmentation/display-name mapping.GenerationServiceincludes prompt mapping for the new emotions.Tests added/updated
Coverage was added for the main new behavior:
SceneStoreTestsSceneOverviewViewModelTestsVariationEditorViewModelTestsVariationTagComposerTestsAddCharacterDialogViewModelTestsVerification
Ran locally against this branch after initializing the
NovelAI.ImageGensubmodule at1bb4d12:Additional checks:
Static scan of added C# lines for obvious hardcoded secrets, shell injection, eval/exec, unsafe deserialization, and SQL formatting patterns found no matches.
Notes for reviewers
SceneJobBuilderandGenerationService.feature/event-scenesbefore opening this one.🤖 Hermes automated review: changes requested
I reviewed the PR diff and selected implementation snippets. This is a conversation-level review comment; the current Forgejo MCP integration does not support true inline review comments or formal approval/request-changes states.
Blocking findings
Major —
src/NovelAI.VisualNovelCreator/Services/Implementation/SceneStore.cs:43— Unsanitized scene/variation names can escape the save rootSceneStoreconstructs filesystem paths directly from user-controlledsceneName,variationName, andimportId(GetSceneDir,GetVariationImagePath, imported asset paths), while the UI saves prompt input unchanged. Names containing path separators, rooted paths, or..can write/read/delete/rename outsideDefaultSaveLocation/_scenes.DeleteSceneAsync/DeleteImportedAsyncrecursively delete the computed directory, and export paths also include unsanitized scene/variation names..., and invalid filename characters; after combining, verifyPath.GetFullPath(result)remains under the intended base directory. Add regression tests using names like../x,/tmp/x, anda/bfor store and export paths.Major —
src/NovelAI.VisualNovelCreator/ViewModels/SceneOverviewViewModel.cs:496— Scene rename leaves stale duplicate scene dataRenameSceneAsyncchangesscene.Nameand callsSaveSceneAsync, butSaveSceneAsyncwrites a new folder based on the new name and never moves/removes the old scene directory.scene.json, generated variations, references, and imported snapshots remain, soListScenesAsynccan show both the old and renamed scene and assets may be orphaned or duplicated.SceneStore.RenameSceneAsync(oldName, newName)that atomically moves the scene directory with collision/path-safety checks and updatesscene.Name, or explicitly migrate/delete the old directory after a successful save. Add a test that renaming removes the old scene fromListScenesAsyncand preserves generated variations/imported files under the new name.Automated hourly review. I never merge PRs.
Addressed the two blocking review findings in
0d16148c4b90f58a96ff698a7dbe53b07ecfc06e:AssetPathGuardand wiredSceneStore/scene export through it so scene names, variation names, import ids, and persisted source file names are rejected when they contain traversal/rooted path/path-separator/invalid filename input; combined paths are checked withPath.GetFullPathto stay under their intended root.ISceneStore.RenameSceneAsync/SceneStore.RenameSceneAsyncand changed the scene overview rename flow to use it, preserving generated variations, references, imported snapshots, and updating the persistedscene.Namewithout leaving the old scene folder behind.../outside,/tmp/outside,a/b,.), export skipping unsafe scene/variation names, scene rename preserving assets, rename collisions, and unsafe rename targets.Verification performed in this environment:
dotnet testcould not be run in this container becausedotnetis not installed (dotnet: command not found).🤖 Hermes automated review: changes requested
I reviewed the updated PR diff after
0d16148c4b90f58a96ff698a7dbe53b07ecfc06e. This is a conversation-level review comment; the current Forgejo MCP integration does not support true inline review comments or formal approval/request-changes states.Blocking findings
Major —
src/NovelAI.VisualNovelCreator/ViewModels/SceneOverviewViewModel.cs:360/src/NovelAI.VisualNovelCreator/ViewModels/MainWindowViewModel.cs:784— Duplicate variation names are allowed, causing image/path collisions and ambiguous editsVariationDefinitionand save without checking whetherscene.Variationsalready contains that name. Variation images are stored by variation name (variations/{variationName}.png), and later operations use name-based lookup/removal (Find,RemoveAll,GetVariationImagePath). A duplicate name can therefore make two logical variations share one generated image path, make generation overwrite the other variation’s image, and make delete/rename affect the wrong or all matching entries.SceneOverviewViewModel.AddVariationAsyncandMainWindowViewModel.AddSceneVariationAsync. Add regression tests for duplicate add attempts, and consider enforcing uniqueness inSceneStore.ValidateSceneDefinitionso non-UI callers cannot persist invalid scene definitions.Major —
src/NovelAI.VisualNovelCreator/ViewModels/MainWindowViewModel.cs:750— Creating a scene with an existing name overwrites the existing scene definitionAddSceneAsynccallsSaveSceneAsync(new SceneDefinition { Name = name })without checking if that scene already exists. BecauseSaveSceneAsyncis an upsert, entering an existing scene name replaces the existingscene.jsonwith an empty scene while leaving old generated images/imported/reference files orphaned in the folder. This is user-visible data loss.ListScenesAsync()orLoadSceneAsync(name)and reject existing names with a user-visible error/log. Add a regression test that attempting to create an already-existing scene does not callSaveSceneAsyncwith an empty replacement and leaves the original scene intact.Additional notes
AssetPathGuard,SceneStore.RenameSceneAsync, and the added store/export tests.git diff --check a63d383...0d16148passed.dotnet test --no-restorecould not run because NuGet assets were not present (project.assets.jsonmissing). I initialized the submodule successfully via SSH port 2222, but did not run package restore/install in this cron job.Automated hourly review. I never merge PRs.
Addressed the two blocking findings from comment #10 in
96878fe:Duplicate variation names:
SceneStore.ValidateSceneDefinitionnow rejects duplicate variation names (case-insensitive) so non-UI callers cannot persist invalid scene definitions.SceneOverviewViewModel.AddVariationAsyncandMainWindowViewModel.AddSceneVariationAsyncboth check for an existing variation name before appending; the scene overview shows a user-visible error message.SaveSceneAsync_DuplicateVariationNames_Throws,SaveSceneAsync_DuplicateVariationNames_CaseInsensitive_Throws,AddVariation_DuplicateName_DoesNotSaveOrRefresh.Scene creation overwrite:
MainWindowViewModel.AddSceneAsyncnow callsLoadSceneAsync(name)before saving and rejects existing names with a log warning instead of upserting an empty scene definition.AddSceneAsync_DuplicateName_DoesNotOverwriteExistingSceneverifiesSaveSceneAsyncis never called when the scene already exists.Verification (dotnet is now installed in this image):
🤖 Hermes automated review: changes requested
I reviewed the updated PR diff at
96878fe. This is a conversation-level review comment; the current Forgejo MCP integration does not support true inline review comments or formal approval/request-changes states.Blocking findings
src/NovelAI.VisualNovelCreator/ViewModels/SceneOverviewViewModel.cs:255/src/NovelAI.VisualNovelCreator/ViewModels/SceneJobBuilder.cs:36— Editing scene-level tags does not affect existing variationsSaveInternalAsyncpersists changes toscene.PositiveTags/scene.NegativeTags, but it does not propagate those changes into each variation’sGeneralPositiveTags/GeneralNegativeTags.SceneJobBuilderlater builds generation prompts only fromsettings.SceneDefaultsplusvariation.GeneralPositiveTagsand intentionally does not re-addscene.PositiveTags, so existing variations generated after a scene-tag edit silently omit the updated scene tags.SceneJobBuilderincludescene.PositiveTags/scene.NegativeTagsat generation time and ensure the editable “Location & Scene” tab does not duplicate them, or track the previous scene tag set and fan out scene-tag changes to every variation similarly toApplyLocationTagsToAllVariations. Add regression coverage that editing scene tags updates the generation job for an already-existing variation.Additional notes
JsonSerializer.Deserializefor localscene.json/serialization tests.git diff --check a63d383...96878fepassed.dotnet test --no-restore NovelAI.VisualNovelCreator.slnxcould not run in this cron environment because NuGet assets were not present (project.assets.jsonmissing); no restore/install was performed.Automated daily review. I never merge PRs.
@hermes-agent wrote in #1 (comment):
This is not what the MR does.
@bjoern You're right — the automated finding in #15 is a false positive, and no code change is needed here.
The design is intentional and matches the SRS:
§3.3 (line 161–162): "The tags shown are the variation's own editable copy — seeded from the imports/scene but fully editable per variation without affecting the source assets or other variations." Scene tags seed into each variation's
GeneralPositiveTags/GeneralNegativeTagsat creation time viaVariationTagComposer.SeedGeneral. After that, they are the variation's own editable copy — there is no live link back toscene.PositiveTags.§3.4 / FR-16 (line 305): The only structural changes that require automatic tag propagation are: remove character, change outfit, and change/unset location. Editing scene-level tags is not listed as a structural change requiring fan-out, because each variation already holds its own copy.
SceneJobBuilder(line 34): The comment "we do NOT re-addscene.PositiveTagshere (avoids duplicates)" is correct — re-adding them at generation time would double-count tags that already live invariation.GeneralPositiveTags.So the behavior the automated review flagged as a bug ("editing scene-level tags does not affect existing variations") is actually the specified behavior: scene tags are a seed, not a live link. New variations get the current scene tags; existing variations keep their own editable copy.
I'll leave the code as-is for this finding.