Skip to content

feat(studio-v2): Studio V2 — text style pipeline, layout redesign, canvas handles - #9

Merged
deckyfx merged 2 commits into
masterfrom
feat/studio-v2
Jul 31, 2026
Merged

feat(studio-v2): Studio V2 — text style pipeline, layout redesign, canvas handles#9
deckyfx merged 2 commits into
masterfrom
feat/studio-v2

Conversation

@deckyfx

@deckyfx deckyfx commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Summary

  • Phase 1 (server): EF Core migration adds 5 text-style columns (font_color, stroke_color, stroke_width, rotation, text_align) to PageTranslationLog. TypesettingService burns all five styles onto the result image. PortalRoutes PATCH + PageTranslationService accept and persist them. New POST /api/portal/jobs/{id}/inpaint endpoint queues async white-fill inpaint through the existing InferenceQueue BackgroundService.

  • Phase 2 (extension bridge): content.ts listens for window.postMessage({ type: "web-ocr:image-updated" }) from Studio and relays it to background.tsreplacePageImages in the active tab. New ImageUpdatedMsg / ImageUpdatedRelayMsg types. Fixed pre-existing TS2339 (src.datasetimg.dataset).

  • Phase 3 (Studio V2 layout): StudioPage fully rewritten — STAGE_ORDER + createMemo sorts active stages so the earlier pipeline stage always renders on the left. Left/right panels are independently collapsible. Left panel splits into a Stage 1 section (BubbleList) and a Stage 3 section (text overlay list), stacked when both are active. panelContext signal drives context-sensitive right panel (BubbleEditor vs new TextStyleEditor). Toolbar shows Stage 1 actions (Detect, Inpaint) and/or Stage 3 actions (Auto Texts, Burn Texts) based on active stages. handleBurnTexts posts web-ocr:image-updated after burn. New TextStyleEditor component covers font, stroke, text-align, and rotation.

  • Phase 4 (canvas rotation handle): BubbleCanvas gains a DragRotating state and optional onRotate prop. A dashed stem + circle handle appears above the selected overlay's top-centre in Stage 3; dragging it rotates the overlay with a live angle badge. The bubble <g> receives an SVG rotate(deg cx cy) transform only in showTextOverlay mode so Stage 1 detection boxes are unaffected. Stage 3 move and resize now call real server handlers. Text overlay preview respects all bubble style fields.

Test plan

  • Server builds: dotnet build WebOcr.slnx
  • Server EF migration applies cleanly: dotnet ef database update
  • Web client typechecks: cd server && bun run typecheck
  • Web client builds: cd server && bun run build
  • Extension typechecks + builds: cd extension && bun run build
  • Studio: toggle Stage 1 + Stage 3 — earlier stage always on left
  • Studio: collapse/expand left and right panels
  • Studio Stage 1: Detect button re-runs detection; Inpaint button white-fills bubbles
  • Studio Stage 3: Auto Texts re-detects + retranslates; Burn Texts produces result and fires postMessage
  • Studio Stage 3: select overlay → TextStyleEditor appears in right panel; editing fields persists to server
  • Studio Stage 3 canvas: drag overlay to move; drag resize handles to resize; drag rotation handle to rotate — all persist to server
  • Extension: after Burn Texts in Studio, the source page image updates in the active tab

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a multi-stage Studio workflow for Original, Inpainted, Compose, and Result views.
    • Added inpainting, automatic text regeneration, burn-text actions, and stage-specific controls.
    • Added text styling for font, color, outline, alignment, and rotation.
    • Added canvas rotation handles, collapsible editing panels, and read-only result stages.
    • Updated the browser extension to refresh translated images automatically after Studio changes.
  • Bug Fixes

    • Improved image refresh behavior to prevent stale results from being displayed.

…nvas handles

Phase 1 — server text-style pipeline:
- EF Core migration adds font_color, stroke_color, stroke_width, rotation,
  text_align columns to PageTranslationLog
- TypesettingService applies all five style fields when burning text onto
  the inpainted image (SkiaSharp: color, stroke, rotation, alignment)
- PageTranslationService maps new fields in MapBubble; PortalRoutes PATCH
  accepts and persists them via UpdateBubbleBody
- POST /api/portal/jobs/{id}/inpaint runs white-fill inpaint asynchronously
  through the InferenceQueue BackgroundService (status: processing→done/error)

Phase 2 — extension postMessage bridge:
- content.ts listens for window.postMessage web-ocr:image-updated from Studio,
  relays to background as image-updated-relay
- background.ts handles relay → replacePageImages in the active tab
- types.ts: ImageUpdatedMsg + ImageUpdatedRelayMsg added
- Fix TS2339: src.dataset → img.dataset in replacePageImages

Phase 3 — Studio V2 frontend layout:
- StudioPage fully rewritten: STAGE_ORDER constant + sortedActiveStages memo
  always places the earlier pipeline stage on the left regardless of toggle order
- Left and right panels independently collapsible via chevron buttons
- Split left panel: Stage 1 section (BubbleList) + Stage 3 section (text
  overlay list) stacked vertically when both stages are active
- panelContext signal ("stage1" | "stage3" | null) drives context-sensitive
  right panel: BubbleEditor for stage1 selections, TextStyleEditor for stage3
- Context-sensitive toolbar: Stage 1 → Detect + Inpaint; Stage 3 → Auto Texts
  + Burn Texts; both sets shown simultaneously when both stages are active
- handleBurnTexts posts window.postMessage web-ocr:image-updated after burn
- handleAutoTexts: redetect → poll → retranslate → poll → refetch
- New TextStyleEditor component: font family/size/color, stroke color/width,
  text-align toggle, rotation slider + number input, optional delete button

Phase 4 — Stage 3 canvas rotation handle:
- BubbleCanvas: DragRotating state (centerSvgX/Y, startAngle, initialRotation,
  currentRotation) tracks rotation during drag with live preview
- onRotate optional prop; committed on mouseup
- Rotation handle: dashed stem + white circle above top-centre of bounding box;
  live angle badge (e.g. 45°) shown during drag
- Group SVG transform rotate(deg cx cy) applied only in showTextOverlay mode —
  Stage 1 detection boxes are unaffected
- Stage 3 move and resize now wired to real handlers (previously no-ops)
- Text overlay respects actual bubble styles: fontColor, textAlign,
  fontSizeOverride, -webkit-text-stroke scaled to canvas zoom

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

coderabbitai Bot commented Jul 31, 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: 2027a0b0-1c2b-4520-89ce-048c72766fbe

📥 Commits

Reviewing files that changed from the base of the PR and between 5bfe8cc and 0d268d3.

📒 Files selected for processing (4)
  • extension/src/content.ts
  • server/ClientApp/src/components/BubbleCanvas.tsx
  • server/ClientApp/src/pages/StudioPage.tsx
  • server/src/Services/TypesettingService.cs
🚧 Files skipped from review as they are similar to previous changes (4)
  • extension/src/content.ts
  • server/src/Services/TypesettingService.cs
  • server/ClientApp/src/pages/StudioPage.tsx
  • server/ClientApp/src/components/BubbleCanvas.tsx

📝 Walkthrough

Walkthrough

Studio V2 adds four-stage editing with read-only stages, text styling and rotation controls, backend persistence and rendering, inpainting workflows, and extension-based image refresh notifications.

Changes

Studio V2

Layer / File(s) Summary
Persisted styling and rendering
server/src/Data/*, server/src/Routes/*, server/src/Services/*, server/Migrations/*
Text colors, strokes, alignment, and rotation are persisted, accepted by APIs, rendered, and supported by a new inpainting endpoint.
Canvas rotation and text styling editor
server/ClientApp/src/components/*, server/ClientApp/src/api.ts, server/ClientApp/src/types.ts
The canvas supports rotation gestures and styled overlays. The text editor provides validated text-style controls and deletion.
Stage-aware Studio workflows
server/ClientApp/src/pages/StudioPage.tsx, docs/PLAN_studio_v2.md
StudioPage adds ordered stages, read-only behavior, collapsible panels, stage-specific actions, inpainting, automated text actions, burn-text refreshes, and rotation updates.
Extension image-update bridge
extension/src/*, extension/static/manifest.json, extension/package.json
Image-update events are relayed, broadcast to tabs, and applied to matching page images with cache-busting URLs.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant StudioPage
  participant PortalRoutes
  participant PageTranslationService
  participant TypesettingService
  participant ContentScript
  participant BackgroundScript
  StudioPage->>PortalRoutes: Update bubble styling or start inpainting
  PortalRoutes->>PageTranslationService: Persist styling or run inpainting
  PageTranslationService->>TypesettingService: Render styled translation
  TypesettingService-->>StudioPage: Updated image
  StudioPage->>ContentScript: Post image-updated event
  ContentScript->>BackgroundScript: Relay job ID and result URL
  BackgroundScript->>ContentScript: Broadcast image-updated notification
  ContentScript->>ContentScript: Replace matching page images
Loading

Possibly related PRs

  • deckyfx/web-ocr#3: Extends the same Studio, portal API, rendering, persistence, and extension code paths.
  • deckyfx/web-ocr#7: Extends the same Studio, bubble-editing, rendering, persistence, and extension result-delivery paths.
  • deckyfx/web-ocr#8: Extends the same four-stage Studio and inpainting code paths.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main Studio V2 changes, including text styling, layout redesign, and canvas handles.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.
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
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/studio-v2

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 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: 7

🧹 Nitpick comments (3)
server/ClientApp/src/components/TextStyleEditor.tsx (2)

296-335: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Rotation number input loses intent while typing a negative value.

parseInt("-", 10) || 0 collapses an in-progress - (and -1 → fine, but a cleared field) to 0, snapping the slider and committing 0 on change. Guarding on Number.isNaN keeps the previous value instead.

♻️ Proposed change
-                      onInput={(e) => {
-                        const val = Math.min(180, Math.max(-180, parseInt(e.currentTarget.value, 10) || 0));
-                        setRotation(val);
-                      }}
+                      onInput={(e) => {
+                        const n = parseInt(e.currentTarget.value, 10);
+                        if (Number.isNaN(n)) return;
+                        setRotation(Math.min(180, Math.max(-180, n)));
+                      }}
🤖 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/components/TextStyleEditor.tsx` around lines 296 - 335,
Update the rotation number input handlers in TextStyleEditor’s Rotation control
so an invalid or in-progress value such as "-" does not fall back to 0. Parse
the input, use Number.isNaN to retain the current rotation when parsing fails,
and apply the clamped value only when valid; preserve committing the resulting
rotation in the onChange handler.

267-292: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Icon-only alignment buttons need an explicit accessible name and type="button".

title alone is an unreliable accessible name for screen readers, and buttons without an explicit type default to submit if this editor is ever nested in a form.

♻️ Proposed change
                         <button
+                          type="button"
                           onClick={() => {
                             setTextAlign(align);
                             commit("textAlign", align);
                           }}
                           title={align.charAt(0).toUpperCase() + align.slice(1)}
+                          aria-label={`Align ${align}`}
                           aria-pressed={isActive()}
                         >
🤖 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/components/TextStyleEditor.tsx` around lines 267 - 292,
Update the alignment buttons rendered in the map within TextStyleEditor so each
icon-only button has an explicit accessible name, such as an aria-label derived
from align, and explicitly sets type="button" to prevent form submission.
Preserve the existing title, click behavior, and pressed-state handling.
server/src/Routes/PortalRoutes.cs (1)

393-433: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fourth copy of the same fire-and-forget job scaffolding.

redetect, retranslate, rerender, and now inpaint all repeat identical scope/status/error-handling boilerplate. Consider extracting a local helper so status transitions and error reporting stay consistent as more job actions are added.

♻️ Sketch
static IResult StartJobAction(
    string id, PageTranslationService pipeline, ILogger logger,
    Func<Task> work)
{
    _ = Task.Run(async () =>
    {
        using var scope = pipeline.CreateScope();
        var db2 = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        try
        {
            await SetStatusAsync(db2, id, "processing");
            await work();
            await SetStatusAsync(db2, id, "done");
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Job action failed for {Id}", id);
            try { await SetStatusAsync(db2, id, "error", ex.Message); } catch { }
        }
    });
    return Results.Accepted(null, new { id });
}
🤖 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/Routes/PortalRoutes.cs` around lines 393 - 433, Extract the
repeated fire-and-forget status and error-handling flow from the redetect,
retranslate, rerender, and inpaint route handlers into a shared local helper
such as StartJobAction, with a reusable status-update helper if needed. Update
each handler, including the inpaint endpoint around InpaintOnlyAsync, to
delegate processing, preserve processing/done/error transitions and error
logging, and continue returning Accepted with the job id.
🤖 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 `@docs/PLAN_studio_v2.md`:
- Line 179: Update docs/PLAN_studio_v2.md references to use PUT for the bubble
update endpoint, matching the implemented updateBubble client method and portal
route. Replace the outdated SSE mechanism descriptions in the affected sections
with the current non-SSE behavior, while preserving the existing endpoint paths
and workflow details.

In `@extension/src/content.ts`:
- Around line 339-352: Update replacePageImages so assigning resultUrl only
occurs for images belonging to the updated job, using the job-specific socrJobId
marker or an equivalently job-scoped URL match; remove the broad
src.pathname.includes("/result") condition that affects unrelated jobs.

In `@server/ClientApp/src/components/BubbleCanvas.tsx`:
- Around line 450-472: Update the resize-handle interaction in
handleHandleMouseDown/applyResize to account for the rotation applied by
groupTransform: inverse-rotate pointer drag deltas by -rotation before passing
them to applyResize, while preserving existing behavior for unrotated bubbles
and keeping the handles visually aligned with the rotated group.

In `@server/ClientApp/src/pages/StudioPage.tsx`:
- Around line 149-159: Update handleSelectStage1 and handleSelectStage3 so the
deselection comparison includes panelContext, not only selectedIndex(). Toggle
off only when the clicked index matches the current selection and the active
context is the same stage; otherwise retain the index and switch panelContext to
the clicked stage.
- Around line 576-632: Introduce a shared busy condition for the four stage
actions in StudioPage, combining isRedetecting(), isInpainting(), isAutoTexts(),
and isBurning(). Apply this guard to the disabled conditions of the Detect,
Inpaint, Auto Texts, and Burn Texts buttons so no action can start while any
other job-level action is running, while preserving each action’s existing
loading behavior.

In `@server/src/Services/PageTranslationService.cs`:
- Around line 497-500: Update TypesettingService.RenderOneBubble so its
RenderTextInBubble call forwards BubbleTranslation’s FontColor, StrokeColor,
StrokeWidth, Rotation, and TextAlign values in addition to the existing text,
font, and size fields, ensuring repatch rendering matches full rerender styling.

In `@server/src/Services/TypesettingService.cs`:
- Around line 243-247: Update the canvas setup in the typesetting flow around
ClipRect and RotateDegrees so rotated overlays are not clipped by the unrotated
bubble bounds. When rotation is present, skip the bubble ClipRect or use an
appropriately inflated clip region; preserve the current bubble-bound clipping
behavior when no rotation is applied.

---

Nitpick comments:
In `@server/ClientApp/src/components/TextStyleEditor.tsx`:
- Around line 296-335: Update the rotation number input handlers in
TextStyleEditor’s Rotation control so an invalid or in-progress value such as
"-" does not fall back to 0. Parse the input, use Number.isNaN to retain the
current rotation when parsing fails, and apply the clamped value only when
valid; preserve committing the resulting rotation in the onChange handler.
- Around line 267-292: Update the alignment buttons rendered in the map within
TextStyleEditor so each icon-only button has an explicit accessible name, such
as an aria-label derived from align, and explicitly sets type="button" to
prevent form submission. Preserve the existing title, click behavior, and
pressed-state handling.

In `@server/src/Routes/PortalRoutes.cs`:
- Around line 393-433: Extract the repeated fire-and-forget status and
error-handling flow from the redetect, retranslate, rerender, and inpaint route
handlers into a shared local helper such as StartJobAction, with a reusable
status-update helper if needed. Update each handler, including the inpaint
endpoint around InpaintOnlyAsync, to delegate processing, preserve
processing/done/error transitions and error logging, and continue returning
Accepted with the job id.
🪄 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: ae1c131a-057c-444e-a113-74f443dabe01

📥 Commits

Reviewing files that changed from the base of the PR and between b5b0a40 and 5bfe8cc.

⛔ 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 (18)
  • docs/PLAN_studio_v2.md
  • extension/package.json
  • extension/src/background.ts
  • extension/src/content.ts
  • extension/src/types.ts
  • extension/static/manifest.json
  • server/ClientApp/src/api.ts
  • server/ClientApp/src/components/BubbleCanvas.tsx
  • server/ClientApp/src/components/TextStyleEditor.tsx
  • server/ClientApp/src/pages/StudioPage.tsx
  • server/ClientApp/src/types.ts
  • server/Migrations/20260730234211_AddTextStyleProperties.Designer.cs
  • server/Migrations/20260730234211_AddTextStyleProperties.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 docs/PLAN_studio_v2.md
New component: `server/ClientApp/src/components/TextStyleEditor.tsx`

Fields: textarea for translated text, font family dropdown, font size number, color picker (FontColor), stroke color picker, stroke width, rotation number input, alignment toggle (L/C/R).
On change: calls `PATCH /api/portal/jobs/{id}/bubbles/{index}` with updated fields.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Doc references a PATCH bubble route that doesn't exist.

The implemented endpoint is PUT /api/portal/jobs/{jobId}/bubbles/{bubbleIndex} (server/src/Routes/PortalRoutes.cs Line 153, and updateBubble in server/ClientApp/src/api.ts uses PUT). Lines 179 and 260 say PATCH. Also Lines 194, 243, 246 and 285 still describe an "SSE" mechanism that Line 261 correctly says isn't needed.

Also applies to: 259-261

🤖 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 `@docs/PLAN_studio_v2.md` at line 179, Update docs/PLAN_studio_v2.md references
to use PUT for the bubble update endpoint, matching the implemented updateBubble
client method and portal route. Replace the outdated SSE mechanism descriptions
in the affected sections with the current non-SSE behavior, while preserving the
existing endpoint paths and workflow details.

Comment thread extension/src/content.ts Outdated
Comment thread server/ClientApp/src/components/BubbleCanvas.tsx
Comment thread server/ClientApp/src/pages/StudioPage.tsx
Comment thread server/ClientApp/src/pages/StudioPage.tsx
Comment thread server/src/Services/PageTranslationService.cs
Comment thread server/src/Services/TypesettingService.cs Outdated
- content.ts: scope replacePageImages to socrJobId only; drop broad
  `/result` path match that could overwrite images from other jobs
- BubbleCanvas: store bubble rotation in DragResizing; inverse-rotate
  drag delta before applyResize so handles behave correctly on rotated
  Stage-3 overlays
- StudioPage: handleSelectStage1/Stage3 deselect only when both index
  AND panelContext match, preventing spurious deselect when switching
  the same bubble between Stage 1 and Stage 3
- StudioPage: cross-stage busy guard — all four action buttons now
  disable while any one job-level action is running
- TypesettingService.RenderOneBubble: forward FontColor, StrokeColor,
  StrokeWidth, Rotation, TextAlign to RenderTextInBubble so per-bubble
  repatch renders identically to full rerender
- TypesettingService.RenderTextInBubble: skip axis-aligned ClipRect
  when rotation is applied; clipping in unrotated space was chopping
  off rotated glyphs at bubble-box edges

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

deckyfx commented Jul 31, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 31, 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 642dcb7 into master Jul 31, 2026
1 check passed
@deckyfx
deckyfx deleted the feat/studio-v2 branch July 31, 2026 09:45
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