Skip to content

feat(studio): 4-stage inpaint pipeline with compose view - #8

Merged
deckyfx merged 3 commits into
masterfrom
feat/inpainted-comparison
Jul 30, 2026
Merged

feat(studio): 4-stage inpaint pipeline with compose view#8
deckyfx merged 3 commits into
masterfrom
feat/inpainted-comparison

Conversation

@deckyfx

@deckyfx deckyfx commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Summary

  • Save inpainted.png per job — new WhiteFillAll method cleanly separates the inpainting stage (stage 2) from text rendering (stage 4). A real LaMa/diffusion model can replace the white-fill placeholder without structural changes to the pipeline.
  • Text-only rendering on inpainted base — new RenderTextOnly() draws text glyphs with no white-fill; TranslatePageAsync step 4 now calls it with inpaintedPng as the base instead of RenderTranslations on the original. Eliminates the white-box double-fill artifact in result.png.
  • RerenderAsync backward-compatible — reads inpainted.png as base if it exists, falls back to original.png + RenderTranslations for jobs created before this change.
  • DB migration — adds InpaintedImagePath column to PageTranslationJob; EF migration included.
  • Portal routeGET /api/portal/jobs/{id}/inpainted serves the inpainted image.
  • 4-stage Studio UI — replaces the old ViewMode toggle with a stage picker (1·Original, 2·Inpainted, 3·Compose, 4·Result). Click to toggle; max 2 active at once → side-by-side panels. Clicking a third stage replaces the oldest.
  • Compose stage — shows inpainted.png as the background with translated text overlaid live via SVG <foreignObject> (transparent background, no burn) so the user can review/adjust layout before committing to Re-render.
  • showTextOverlay prop on BubbleCanvas — renders translatedText centered inside each non-excluded bubble with no white fill behind the glyphs.

Test plan

  • Upload a manga page → confirm inpainted.png is saved alongside result.png
  • Verify result.png has text rendered on clean white-filled bubbles (no double white-box)
  • Open Studio → cycle through all 4 stage buttons, verify correct image shown for each
  • Select 2 stages simultaneously → confirm side-by-side layout with correct labels
  • Select 3rd stage → confirm oldest is replaced (max 2 active)
  • Switch to Compose (stage 3) → confirm text overlays appear over inpainted.png with no white fill behind text
  • Run Re-render on an existing job → confirm RerenderAsync uses inpainted.png as base
  • Run Re-render on a job without inpainted.png → confirm fallback to original + white-fill still works
  • Check GET /api/portal/jobs/{id}/inpainted returns 200 with valid PNG

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a stage picker to view up to two stages side by side (original, inpainted, compose, final) with “not ready yet” placeholders.
    • Added an optional translated-text overlay inside bubbles, including per-bubble font sizing.
    • Added support for retrieving and displaying inpainted results.
  • Improvements

    • Enhanced rerendering to preserve existing artwork while regenerating bubble text and refreshing related images reliably.
    • Improved inpainting behavior by using stored bubble geometry, including scenarios where OCR finds no text.

Pipeline (C#):
- Save white-fill inpainted.png per job (WhiteFillAll); architecture
  cleanly separates inpainting (stage 2) from text rendering (stage 4)
  so a real LaMa/diffusion model can drop in without structural changes
- Add RenderTextOnly() — draws text glyphs with no white-fill so the
  bubble background comes entirely from the pre-inpainted image
- TranslatePageAsync step 4 now uses RenderTextOnly(inpaintedPng)
  instead of RenderTranslations(originalPng), eliminating white-box
  double-fill artifacts in result.png
- RerenderAsync reads inpainted.png as base if present; falls back to
  original.png + RenderTranslations for pre-existing jobs
- Add InpaintedImagePath column to PageTranslationJob (EF migration)
- Add GET /api/portal/jobs/{id}/inpainted route

Studio UI (SolidJS):
- Replace ViewMode with Stage: original | inpainted | compose | result
- Toolbar shows 4 stage buttons (1·Original … 4·Result); click to
  toggle, max 2 active simultaneously → side-by-side panels
- Compose stage (3) shows inpainted.png as background with translated
  text overlaid live via SVG foreignObject (transparent bg, no burn)
- Add showTextOverlay prop to BubbleCanvas; renders translatedText
  centered inside each non-excluded bubble with no white fill

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 94f81373-fd57-4e0d-83d9-543676c15228

📥 Commits

Reviewing files that changed from the base of the PR and between 11a61a8 and 6608b56.

📒 Files selected for processing (1)
  • server/src/Services/PageTranslationService.cs

📝 Walkthrough

Walkthrough

The translation pipeline now stores an inpainted image separately from the final result. A new portal endpoint and client URL expose it, while Studio supports selecting up to two pipeline stages and BubbleCanvas can display translated text overlays.

Changes

Inpainted rendering pipeline

Layer / File(s) Summary
Inpainting and text-only rendering
server/src/Services/TypesettingService.cs, server/src/Services/PageTranslationService.cs
Separates bubble white-filling from text rendering, produces inpainted.png from all bubble regions, renders translated text on top, and reuses the inpainted base during rerendering.
Inpainted image persistence and retrieval
server/src/Data/AppDbContext.cs, server/Migrations/*, server/src/Routes/PortalRoutes.cs, server/ClientApp/src/api.ts, server/ClientApp/src/types.ts
Adds the nullable InpaintedImagePath database field, migration mappings, job finalization persistence, and a PNG-serving portal endpoint with a client URL helper.

Studio stage viewer

Layer / File(s) Summary
Stage selection and rendering
server/ClientApp/src/pages/StudioPage.tsx
Replaces view modes with a picker for up to two stages, renders original, inpainted, compose, and result panels, and shares cache-busting across image URLs.
Translated text overlay
server/ClientApp/src/components/BubbleCanvas.tsx
Adds an optional, pointer-transparent overlay that displays translated text inside eligible bubbles with bubble-specific typography.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant StudioPage
  participant PortalRoutes
  participant PageTranslationService
  participant TypesettingService
  participant PageTranslationJob
  PageTranslationService->>TypesettingService: WhiteFillAll(originalPng, bubbleRegions)
  TypesettingService-->>PageTranslationService: inpaintedPng
  PageTranslationService->>TypesettingService: RenderTextOnly(inpaintedPng, translations)
  TypesettingService-->>PageTranslationService: resultPng
  PageTranslationService->>PageTranslationJob: Persist image paths
  StudioPage->>PortalRoutes: Request selected inpainted stage
  PortalRoutes-->>StudioPage: Return inpainted PNG
Loading

Possibly related PRs

  • deckyfx/web-ocr#5: Both changes modify the page translation pipeline in PageTranslationService.cs.
  • deckyfx/web-ocr#7: Both changes use per-bubble typography settings in translated text rendering.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: a four-stage inpaint pipeline with compose view support in Studio.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/inpainted-comparison

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/ClientApp/src/pages/StudioPage.tsx (1)

257-264: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Refetch the job after rerender, redetect, and retranslate complete.

The page polls for completion with standalone getJob calls, but the reactive job() resource used by the stage renderers is not updated. This leaves the UI showing stale placeholders until another resource refresh or page reload.

Proposed fix
-  const [job] = createResource(() => params.id, getJob);
+  const [job, { refetch: refetchJob }] = createResource(() => params.id, getJob);
...
       await pollUntilDone();
       await refetchBubbles();
+      await refetchJob();
       setImageVersion((v) => v + 1);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/ClientApp/src/pages/StudioPage.tsx` around lines 257 - 264, Update
handleRerender so the reactive job() resource is explicitly refetched after
rerenderJob, pollUntilDone, and refetchBubbles complete. Reuse the existing job
resource’s refetch mechanism, ensuring stage renderers receive the latest job
state immediately while preserving the current image-version update flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/ClientApp/src/components/BubbleCanvas.tsx`:
- Around line 534-550: Update the preview style in BubbleCanvas’s bubble
renderer to use the per-bubble fontFamily value and derive font-size from
fontSizeOverride multiplied by layout().scale when an override exists, while
retaining the current height-based fallback otherwise.

In `@server/ClientApp/src/pages/StudioPage.tsx`:
- Around line 421-444: Update the stage toggle buttons rendered in the
ALL_STAGES loop to include aria-pressed bound to isActive(), exposing each
button’s selected state to assistive technology while preserving the existing
toggle behavior.

In `@server/src/Services/PageTranslationService.cs`:
- Around line 263-272: The rerender path must rebuild the inpainted base from
original.png before rendering current bubble data. In the flow containing
basePath and RenderTextOnly, regenerate and persist inpainted.png under the
existing lock, then read that freshly generated image and pass it to
RenderTextOnly; preserve the original-image fallback and RenderTranslations
behavior for jobs without an inpainted image.
- Around line 152-155: Update the inpainting call in the PageTranslationService
flow to pass an input derived from all detected bubbles in bubbles, rather than
the translations collection that excludes failed or empty OCR results. Preserve
the existing WhiteFillAll, file-writing, and path-generation behavior.

---

Outside diff comments:
In `@server/ClientApp/src/pages/StudioPage.tsx`:
- Around line 257-264: Update handleRerender so the reactive job() resource is
explicitly refetched after rerenderJob, pollUntilDone, and refetchBubbles
complete. Reuse the existing job resource’s refetch mechanism, ensuring stage
renderers receive the latest job state immediately while preserving the current
image-version update flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: be38da88-271a-42ec-b2d4-e173e446a60c

📥 Commits

Reviewing files that changed from the base of the PR and between 37c8cea and b945ac3.

⛔ Files ignored due to path filters (2)
  • server/wwwroot/js/app.css is excluded by !server/wwwroot/js/app.css
  • server/wwwroot/js/app.js is excluded by !server/wwwroot/js/app.js
📒 Files selected for processing (11)
  • server/ClientApp/src/api.ts
  • server/ClientApp/src/components/BubbleCanvas.tsx
  • server/ClientApp/src/pages/StudioPage.tsx
  • server/ClientApp/src/types.ts
  • server/Migrations/20260730105929_AddInpaintedImagePath.Designer.cs
  • server/Migrations/20260730105929_AddInpaintedImagePath.cs
  • server/Migrations/AppDbContextModelSnapshot.cs
  • server/src/Data/AppDbContext.cs
  • server/src/Routes/PortalRoutes.cs
  • server/src/Services/PageTranslationService.cs
  • server/src/Services/TypesettingService.cs

Comment thread server/ClientApp/src/components/BubbleCanvas.tsx
Comment thread server/ClientApp/src/pages/StudioPage.tsx
Comment thread server/src/Services/PageTranslationService.cs Outdated
Comment thread server/src/Services/PageTranslationService.cs Outdated
- PageTranslationService: pass all detected bubbles (not just those with
  translations) to WhiteFillAll so OCR-failed bubbles still get their
  background cleaned
- RerenderAsync: regenerate inpainted.png from current bubble positions
  before rendering text, so moved/resized boxes are reflected in the
  clean background layer; persist updated InpaintedImagePath to DB
- handleRerender: refetch job resource alongside bubbles after re-render
  so stage renderers see the latest inpaintedImagePath/resultImagePath
- BubbleCanvas compose overlay: respect bubble.fontFamily and derive
  font-size from fontSizeOverride * layout().scale when override is set
- Stage picker buttons: add aria-pressed bound to isActive()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@deckyfx

deckyfx commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/src/Services/PageTranslationService.cs`:
- Around line 264-272: Update TranslatePageAsync so every detected bubble
persists an empty translation log, including OCR failure and empty-text early
exits, before returning. Reuse LogBubbleAsync for these paths and ensure the
stored BubbleX, BubbleY, BubbleW, BubbleH, and Confidence values allow the
allBubbleLogs/allBubbleFills flow in PageTranslationService to include the
geometry during rerender.
- Around line 294-301: Update the regeneration flow around allBubbleFills and
the basePng/resultPng selection so an existing inpaintedPath is used only when
current bubble fills were generated. When allBubbleFills.Count is zero, clear
InpaintedImagePath and delete the stale inpainted file, then use originalPng and
the legacy RenderTranslations path; preserve the current inpainted rendering
path when valid fills exist.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e6ba929c-db60-42d5-bdba-d87b4efa3773

📥 Commits

Reviewing files that changed from the base of the PR and between b945ac3 and 11a61a8.

📒 Files selected for processing (3)
  • server/ClientApp/src/components/BubbleCanvas.tsx
  • server/ClientApp/src/pages/StudioPage.tsx
  • server/src/Services/PageTranslationService.cs

Comment thread server/src/Services/PageTranslationService.cs
Comment on lines +294 to +301
// Render text on the freshly regenerated inpainted base, or fall back
// to the legacy white-fill+text path for jobs without bubble data.
var basePng = File.Exists(inpaintedPath)
? await File.ReadAllBytesAsync(inpaintedPath, ct)
: originalPng;
var resultPng = File.Exists(inpaintedPath)
? await Task.Run(() => typesetter.RenderTextOnly(basePng, translations, padding), ct)
: await Task.Run(() => typesetter.RenderTranslations(basePng, translations, padding), ct);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not reuse a stale inpainted file when no current bubble fills exist.

When all current bubbles are excluded or absent, regeneration is skipped, but File.Exists(inpaintedPath) still selects an older inpainted.png. Rerender then produces a stale white-filled result instead of falling back to original.png, while the Studio inpainted stage may remain advertised. Gate base selection on current inpaint data and clear the persisted inpainted path when none exists.

Proposed fix
+            var hasCurrentInpainted =
+                allBubbleFills.Count > 0 && File.Exists(inpaintedPath);
             var basePng   = File.Exists(inpaintedPath)
-                ? await File.ReadAllBytesAsync(inpaintedPath, ct)
+                ? await File.ReadAllBytesAsync(inpaintedPath, ct)
                 : originalPng;
-            var resultPng = File.Exists(inpaintedPath)
+            var resultPng = hasCurrentInpainted
                 ? await Task.Run(() => typesetter.RenderTextOnly(basePng, translations, padding), ct)
                 : await Task.Run(() => typesetter.RenderTranslations(basePng, translations, padding), ct);

Also clear InpaintedImagePath and remove the stale file when allBubbleFills.Count == 0.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/Services/PageTranslationService.cs` around lines 294 - 301, Update
the regeneration flow around allBubbleFills and the basePng/resultPng selection
so an existing inpaintedPath is used only when current bubble fills were
generated. When allBubbleFills.Count is zero, clear InpaintedImagePath and
delete the stale inpainted file, then use originalPng and the legacy
RenderTranslations path; preserve the current inpainted rendering path when
valid fills exist.

- Log bubble geometry for OCR-failure and empty-text paths so
  RerenderAsync can still inpaint those bubbles from the DB.
- Clear stale inpainted.png (file + DB column) in RerenderAsync when
  allBubbleFills is empty, preventing RenderTextOnly from running on a
  stale inpainted base.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@deckyfx

deckyfx commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@deckyfx
deckyfx merged commit b5b0a40 into master Jul 30, 2026
1 check passed
@deckyfx
deckyfx deleted the feat/inpainted-comparison branch July 30, 2026 18:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant