Skip to content

feat(inpaint): TextSeg-guided two-pass inpainting with pixel-accurate erasure - #11

Merged
deckyfx merged 2 commits into
masterfrom
feat/text-seg-inpaint
Aug 11, 2026
Merged

feat(inpaint): TextSeg-guided two-pass inpainting with pixel-accurate erasure#11
deckyfx merged 2 commits into
masterfrom
feat/text-seg-inpaint

Conversation

@deckyfx

@deckyfx deckyfx commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Summary

  • ComicTextDetector (TextSeg) service — ONNX model producing a per-pixel text mask and bounding boxes for all text regions (narration, SFX, speech bubbles)
  • Two-pass inpainting pipeline:
    • Pass 0 (Telea via OpenCvSharp4): erases non-bubble text blocks (narration boxes, SFX) using bounding-box mask; surrounding context reconstructs dark/light backgrounds naturally
    • Pass 1 (LaMa ONNX + TextSeg pixel mask): erases text inside speech bubbles per-pixel; IsInsideAnyBubble() centre-point test separates bubble vs non-bubble blocks
  • MaskComposite fix in InpaintPageWithPixelMask: previous CopyRegion overwrote the entire bubble bounding box (destroying bubble outlines and artwork); now only writes pixels where rawMask is true, preserving all non-text pixels
  • Studio: bubble visibility toggle now forwarded to the Inpainted stage; TextSeg block overlay for OCR/inpaint region visualisation

Test plan

  • Submit a manga page with speech bubbles — verify text is erased, bubble outlines and artwork are preserved (no white blobs)
  • Submit a page with narration boxes on dark backgrounds — verify Telea reconstructs the dark background (no white rectangles)
  • Submit a page with SFX outside bubbles — verify they are erased by Pass 0
  • Toggle bubble visibility in Studio → Inpainted stage — verify the toggle works
  • Confirm /health correctly reports TextSegReady state

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added automatic text-region detection to improve translation, typesetting, and text removal.
    • Added Studio controls to view, select, and remove detected text regions.
    • Added mask-based text removal for more precise image cleanup.
    • Added fallback processing when text-region detection is unavailable.
    • Added model readiness information to system health status.
  • Chores

    • Updated the extension version to 1.3.7.

… erasure

Introduces a full text-erasure pipeline powered by ComicTextDetector (TextSeg):

**TextSeg service**
- Add TextSegmentationService running comictextdetector ONNX model
- Outputs per-pixel sigmoid mask (text on white) + bounding boxes for all text blocks
- TextSeg runs first so its results feed both OCR and inpainting

**Two-pass inpainting**
- Pass 0 — Telea (OpenCvSharp4): erases non-bubble TextSeg blocks (narration
  boxes, SFX, panel labels) using bounding-box mask; surrounding dark/light
  context is naturally reconstructed, no white rectangles
- Pass 1 — LaMa ONNX with TextSeg pixel mask: erases text inside speech
  bubbles per-bubble using the precise pixel mask, not the bounding box

**MaskComposite fix**
- InpaintPageWithPixelMask previously used CopyRegion (whole bounding box)
  which overwrote bubble outlines and artwork with LaMa's reconstruction
- Replaced with MaskComposite: only writes pixels where rawMask is true,
  preserving every non-text pixel (bubble borders, panel art, backgrounds)

**Studio improvements**
- BubbleCanvas showBubbles prop now correctly forwarded to the Inpainted stage
  so the bubble visibility toggle works in all Studio views
- TextSeg block overlay for OCR/inpaint region visualisation
- Bubble box management UI in Studio

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

coderabbitai Bot commented Aug 11, 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: b2b75f84-7fd8-4385-9afc-a045f6a433a9

📥 Commits

Reviewing files that changed from the base of the PR and between 3ba3e96 and 3513b5e.

📒 Files selected for processing (6)
  • server/ClientApp/src/pages/StudioPage.tsx
  • server/server-csharp.csproj
  • server/src/BootExtensions.cs
  • server/src/Services/PageTranslationService.cs
  • server/src/Services/TextSegmentationService.cs
  • server/src/Services/TypesettingService.cs
🚧 Files skipped from review as they are similar to previous changes (6)
  • server/server-csharp.csproj
  • server/ClientApp/src/pages/StudioPage.tsx
  • server/src/Services/TextSegmentationService.cs
  • server/src/BootExtensions.cs
  • server/src/Services/TypesettingService.cs
  • server/src/Services/PageTranslationService.cs

📝 Walkthrough

Walkthrough

The extension version changes to 1.3.7. The server adds TextSeg model management, ONNX segmentation, text-block APIs, canvas overlays, mask caching, and mask-based translation and inpainting.

Changes

Text segmentation runtime

Layer / File(s) Summary
Model configuration and startup
server/server-csharp.csproj, server/src/Config.cs, server/src/ModelSettingsStore.cs, server/src/ServiceExtensions.cs, server/src/BootExtensions.cs, server/src/Routes/HealthRoutes.cs
The server configures, downloads, loads, and reports the TextSeg model.
Segmentation inference and worker jobs
server/src/Services/TextSegmentationService.cs, server/src/Workers/InferenceJob.cs, server/src/Workers/InferenceWorker.cs
The server runs ONNX segmentation, creates masks and text blocks, and dispatches segmentation and mask-inpainting jobs.

Text block management

Layer / File(s) Summary
TextSeg block API and canvas integration
server/src/Routes/PortalRoutes.cs, server/ClientApp/src/api.ts, server/ClientApp/src/components/BubbleCanvas.tsx, server/ClientApp/src/pages/StudioPage.tsx, extension/package.json, extension/static/manifest.json
The server exposes cached block retrieval and deletion. The Studio UI loads, displays, selects, hides, and deletes text blocks. Extension metadata changes to version 1.3.7.

Translation and inpainting

Layer / File(s) Summary
Mask-based translation and inpainting
server/src/Services/PageTranslationService.cs, server/src/Services/InpaintService.cs, server/src/Services/TypesettingService.cs
The translation pipeline uses TextSeg blocks and masks for OCR, typesetting, rerendering, and inpainting. Rectangle and flood-fill fallbacks remain available.

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

Sequence Diagram(s)

sequenceDiagram
  participant PageTranslationService
  participant InferenceWorker
  participant TextSegmentationService
  participant InpaintService
  participant TypesettingService
  PageTranslationService->>InferenceWorker: Queue TextSegJob
  InferenceWorker->>TextSegmentationService: SegmentText(imagePng)
  TextSegmentationService-->>InferenceWorker: Return mask and text blocks
  InferenceWorker-->>PageTranslationService: Return TextSegResult
  PageTranslationService->>InpaintService: InpaintPageWithPixelMask
  PageTranslationService->>TypesettingService: TeleaInpaintBlocks
  InpaintService-->>PageTranslationService: Return processed image
Loading

Possibly related PRs

  • deckyfx/web-ocr#10: Extends the same inference and translation components with mask-based text-segmentation inpainting.
  • deckyfx/web-ocr#7: Shares the Studio, rendering, API, and translation workflows extended by this change.
  • deckyfx/web-ocr#3: Shares the server and Studio components used for text-segmentation integration.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.77% 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 clearly summarizes the main change: TextSeg-guided two-pass inpainting with pixel-accurate text erasure.
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/text-seg-inpaint

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

Caution

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

⚠️ Outside diff range comments (1)
server/src/Services/InpaintService.cs (1)

400-411: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the stale summary block left above MaskComposite.

The diff inserts a new <summary> for MaskComposite at lines 404-408 but leaves the previous <summary> at lines 400-403 in place. That older block documents CopyRegion. Two consecutive <summary> elements now attach to MaskComposite, which produces a duplicate-tag documentation warning and describes the wrong behavior. CopyRegion at line 446 is left with no documentation.

♻️ Proposed fix
-    /// <summary>
-    /// Copy a rectangular region from <paramref name="src"/> into <paramref name="dst"/>
-    /// at the specified destination offset. Both bitmaps must be BGRA8888.
-    /// </summary>
     /// <summary>
     /// Copy pixels from <paramref name="src"/> to <paramref name="dst"/> only where
     /// <paramref name="mask"/> (model-space boolean array, size <paramref name="maskSize"/>²)
     /// is <c>true</c>. Coordinates map via <paramref name="scale"/> (crop→model).
     /// </summary>

Then restore the original summary above CopyRegion:

    /// <summary>
    /// Copy a rectangular region from <paramref name="src"/> into <paramref name="dst"/>
    /// at the specified destination offset. Both bitmaps must be BGRA8888.
    /// </summary>
    private static unsafe void CopyRegion(
🤖 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/InpaintService.cs` around lines 400 - 411, Remove the
stale CopyRegion summary immediately before MaskComposite, leaving only
MaskComposite’s mask-compositing documentation there. Restore that
rectangular-copy summary directly above the CopyRegion method declaration so
each method has exactly its own documentation.
🧹 Nitpick comments (7)
server/src/Services/TextSegmentationService.cs (4)

46-58: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Dispose a previous session and publish _session last.

LoadModel runs once from BootBackgroundService today, so this is defensive only. If it is ever called twice, the earlier InferenceSession and its native arena leak. Assigning _session before _inputName also makes IsReady report true for a moment while _inputName is still null.

♻️ Proposed refactor
-        _session   = new InferenceSession(modelPath, opts);
-        _inputName = _session.InputNames[0]; // typically "images" for YOLO-style models
+        var session = new InferenceSession(modelPath, opts);
+        _inputName  = session.InputNames[0]; // typically "images" for YOLO-style models
+        _session?.Dispose();
+        _session    = session;
🤖 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/TextSegmentationService.cs` around lines 46 - 58, Update
TextSegmentationService.LoadModel to create and fully initialize a new
InferenceSession and input name in local variables before publishing them to the
service fields. Dispose the existing _session before replacing it, and assign
_inputName before _session so IsReady cannot observe a partially initialized
model.

115-122: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Derive separate width and height scales for the crop offsets.

mScale is computed from mW only and then applied to padTop and newH as well. The offsets are correct while the model output is square. They shift vertically if mH ever differs from mW. The ternary at Line 118 also returns the same value in both branches.

♻️ Proposed refactor
-        double mScale  = mW == ModelInput ? 1.0 : (double)mW / ModelInput;
-        int    cLeft   = (int)Math.Round(padLeft * mScale);
-        int    cTop    = (int)Math.Round(padTop  * mScale);
-        int    cW      = Math.Max(1, Math.Min(mW - cLeft, (int)Math.Round(newW * mScale)));
-        int    cH      = Math.Max(1, Math.Min(mH - cTop,  (int)Math.Round(newH * mScale)));
+        double sx     = (double)mW / ModelInput;
+        double sy     = (double)mH / ModelInput;
+        int    cLeft  = (int)Math.Round(padLeft * sx);
+        int    cTop   = (int)Math.Round(padTop  * sy);
+        int    cW     = Math.Max(1, Math.Min(mW - cLeft, (int)Math.Round(newW * sx)));
+        int    cH     = Math.Max(1, Math.Min(mH - cTop,  (int)Math.Round(newH * sy)));
🤖 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/TextSegmentationService.cs` around lines 115 - 122, In
the crop calculation, replace the single mScale used by the crop block with
independent width and height scales derived from mW and mH relative to
ModelInput. Apply the width scale to padLeft and newW, and the height scale to
padTop and newH, preserving the existing rounding and clamping behavior in
cLeft, cTop, cW, and cH.

94-110: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Select the mask output by name, not by index.

When neither candidate has one channel, the fallback selects out1, which can be lines_map and produce an incorrect mask. Resolve the output by name, then use the channel count as a fallback. The results[1] and results[2] indexers are supported by ONNX Runtime 1.26.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/TextSegmentationService.cs` around lines 94 - 110, Update
the mask-selection logic in the segmentation output handling to resolve the mask
tensor by its ONNX output name first, rather than assuming a positional index.
Retain channel-count detection as the fallback when names do not identify the
mask, and remove the unsafe fallback that can select lines_map; continue
throwing when no usable mask output is available.

191-214: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use the existing dense output buffer.

AsTensor<float>() returns a DenseTensor<float> for numeric outputs in ONNX Runtime 1.26.0. Cast it, check !IsReversedStride, and read Buffer.Span[y * w + x]. Do not call ToDenseTensor(), because it copies the complete output.

🤖 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/TextSegmentationService.cs` around lines 191 - 214,
Update BuildBinaryMask to accept the DenseTensor<float> output from
AsTensor<float>, validate that IsReversedStride is false, and read values from
Buffer.Span using the y * w + x offset instead of multidimensional indexing.
Avoid ToDenseTensor and preserve the existing thresholding and bitmap-writing
behavior.
server/src/Routes/PortalRoutes.cs (2)

173-176: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the :int route constraint and return the JSON directly.

Sibling routes in this file use {bubbleIndex:int}. Use the same constraint here so non-numeric segments do not reach the handler. Also, the re-serialization step is redundant: the elements are already parsed JSON, so PropertyNamingPolicy has no effect on them, and Deserialize<object> only round-trips the string again.

♻️ Proposed simplification
-        g.MapDelete("/jobs/{id}/textseg-blocks/{index}", async (
+        g.MapDelete("/jobs/{id}/textseg-blocks/{index:int}", async (
             string id, int index,
             AppDbContext db,
             PageTranslationService pipeline) =>
@@
             blocks.RemoveAt(index);
 
-            var updated = System.Text.Json.JsonSerializer.Serialize(blocks,
-                new System.Text.Json.JsonSerializerOptions
-                {
-                    PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.SnakeCaseLower,
-                });
+            var updated = System.Text.Json.JsonSerializer.Serialize(blocks);
             await File.WriteAllTextAsync(cacheFile, updated);
-            return Results.Ok(System.Text.Json.JsonSerializer.Deserialize<object>(updated));
+            return Results.Content(updated, "application/json");

Also applies to: 191-197

🤖 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 173 - 176, Update the
MapDelete route for jobs/{id}/textseg-blocks/{index} and the corresponding
sibling route to constrain the parameter as {index:int}, matching the existing
bubbleIndex route pattern. Return the parsed JSON elements directly from the
handler and remove the redundant Deserialize<object>/re-serialization step and
its unused naming-policy handling.

137-141: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Confirm the cached JSON stays in sync with block deletions and re-runs.

The fast path returns textseg_blocks.json verbatim. PageTranslationService.TranslatePageAsync also writes this file, and the DELETE endpoint rewrites it. Two parallel GET requests on a cold cache can both run segmentation and then write the same file concurrently, which can fail with an IOException or produce a partially written file. Consider serializing the write with the existing per-job lock, or write to a temp file and move it atomically.

♻️ Proposed atomic write
-            await File.WriteAllTextAsync(cacheFile, json);
+            var tmp = cacheFile + ".tmp";
+            await File.WriteAllTextAsync(tmp, json);
+            File.Move(tmp, cacheFile, overwrite: true);
             return Results.Content(json, "application/json");
🤖 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 137 - 141, Make cached JSON
writes in the page translation and block-deletion flows atomic and safe under
concurrent GET requests. Reuse the existing per-job lock when writing
textseg_blocks.json, or write the complete content to a temporary file and
atomically replace the cache file; ensure PortalRoutes’ cache read never
observes partial JSON or fails due to parallel writes.
server/src/Services/PageTranslationService.cs (1)

767-778: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Share the centre-containment test and accept a CancellationToken.

GetTypesettingBox at lines 769-776 and IsInsideAnyBubble at lines 522-526 implement the same centre-in-box test. Extract one helper so the two stay consistent.

LoadTextSegMaskAsync does not accept a CancellationToken, unlike the other async methods in this class. Both call sites, at line 349 and line 424, already hold a ct.

♻️ Proposed refactor
+    /// <summary>Returns true when the centre of <paramref name="region"/> lies inside <paramref name="box"/>.</summary>
+    private static bool CenterInside(BubbleBox region, BubbleBox box)
+    {
+        float cx = region.X + region.Width  / 2f;
+        float cy = region.Y + region.Height / 2f;
+        return cx >= box.X && cx <= box.X + box.Width &&
+               cy >= box.Y && cy <= box.Y + box.Height;
+    }
+
     private static BubbleBox GetTypesettingBox(BubbleBox textRegion, IReadOnlyList<BubbleBox> bubbles)
     {
-        float cx = textRegion.X + textRegion.Width  / 2f;
-        float cy = textRegion.Y + textRegion.Height / 2f;
         foreach (var bubble in bubbles)
-        {
-            if (cx >= bubble.X && cx <= bubble.X + bubble.Width &&
-                cy >= bubble.Y && cy <= bubble.Y + bubble.Height)
-                return bubble;
-        }
+            if (CenterInside(textRegion, bubble)) return bubble;
         return textRegion;
     }
@@
-    private async Task<byte[]?> LoadTextSegMaskAsync(string jobId)
+    private async Task<byte[]?> LoadTextSegMaskAsync(string jobId, CancellationToken ct = default)
     {
         var maskPath = Path.Combine(config.JobsDir, jobId, "textseg_mask.png");
-        return File.Exists(maskPath) ? await File.ReadAllBytesAsync(maskPath) : null;
+        return File.Exists(maskPath) ? await File.ReadAllBytesAsync(maskPath, ct) : null;
     }

Then simplify IsInsideAnyBubble:

    private static bool IsInsideAnyBubble(BubbleBox block, IReadOnlyList<BubbleBox> bubbles) =>
        bubbles.Any(b => CenterInside(block, b));

Also applies to: 784-788

🤖 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 767 - 778,
Extract the shared centre-containment logic from GetTypesettingBox and
IsInsideAnyBubble into one helper, such as CenterInside, and reuse it in both
methods. Update LoadTextSegMaskAsync to accept a CancellationToken and propagate
the existing ct from both call sites through its async operations.
🤖 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/pages/StudioPage.tsx`:
- Around line 132-140: Update the TextSeg loading catch block surrounding
getJobTextSegBlocks to capture the failure and report it through setActionError,
matching the existing error-handling path used by handleDeleteTextSeg, while
preserving setShowTextSeg(false) and the finally cleanup.
- Line 890: Update the delete control’s visibility classes in the StudioPage JSX
so keyboard focus also reveals it, adding a focus-visible-based visibility rule
alongside group-hover:visible while preserving the existing styling and
pointer-hover behavior.
- Around line 874-896: Restructure the TextSeg row around the existing selection
handler so the selectable area is a button, making it keyboard-accessible and
announced as interactive. Move the delete button currently inside the row to be
a sibling outside the selectable button, while preserving event propagation
prevention, styling, and selection behavior without nesting buttons.

In `@server/server-csharp.csproj`:
- Around line 36-37: Add the OpenCvSharp native runtime package references for
the supported Windows and macOS platforms alongside OpenCvSharp4 and the
existing Linux x64 runtime in the project configuration. Use the appropriate
Windows and macOS runtime package variants while preserving the current Linux
runtime reference.

In `@server/src/BootExtensions.cs`:
- Around line 213-229: Update DownloadTextSegAsync to catch failures from
ModelDownloader.EnsureAsync, log a warning with the download context and
exception, and return without rethrowing so the optional TextSeg model failure
does not fault ExecuteAsync or the hosted service. Follow the existing non-fatal
handling pattern used by DownloadDictionaryAsync.

In `@server/src/Routes/PortalRoutes.cs`:
- Around line 172-198: Update the pipeline flow used by TranslatePageAsync and
its OCR/rendering stages so it reads the edited textseg_blocks.json cache and
preserves deleted blocks across subsequent runs; alternatively remove the
MapDelete endpoint if deletion is intentionally display-only. Ensure the
endpoint’s persisted changes are consumed by the actual processing path rather
than only returned in the overlay response.

In `@server/src/Services/PageTranslationService.cs`:
- Around line 196-210: Update the OCR processing flow around GetTypesettingBox,
translations.Add, and LogBubbleAsync to group regions by their typesetting
target before translating. Use value-based BubbleBox grouping, or group by its
X/Y/Width/Height tuple when necessary, join each group’s source text, and
perform one OCR/translation pass using the group’s union rectangle. Emit one
BubbleTranslation and one log row per group so downstream rendering and
rerendering process each bubble only once.
- Around line 349-352: Add a shared cached TextSeg loader in
PageTranslationService that reads textseg_mask.png and deserializes
textseg_blocks.json into BubbleBox values, warning and falling back to an empty
block list if parsing fails. Update RerenderAsync at
server/src/Services/PageTranslationService.cs:349-352 and InpaintOnlyAsync at
server/src/Services/PageTranslationService.cs:424-428 to use this loader and
pass the blocks into TextSegResult, verifying the constructor and BubbleBox
argument order.
- Around line 485-501: Guard the pixel-accurate path in TranslatePageAsync so a
preSeg.Mask is reused only when it has a nonzero length. Treat an empty mask
like an unavailable segmentation result and fall back to the existing
rectangle-mask path instead of passing it to InpaintWithMaskJob; preserve the
current behavior for valid masks and TextSegService-generated masks.
- Around line 79-90: Update the bubble-detection flow around
PageTranslationService’s bubbleTask so BubbleDetectionService.Detect is
submitted through InferenceQueue and processed by InferenceWorker, rather than
invoked via Task.Run; ensure the same queued handling is used by the re-render
path. Preserve result propagation and cancellation while maintaining
single-reader serialization with TextSegJob.

In `@server/src/Services/TextSegmentationService.cs`:
- Around line 145-155: Update BuildLetterbox so a null result from src.Resize
throws an exception instead of skipping canvas.DrawBitmap and returning a blank
LetterboxGray bitmap; preserve the existing draw path for successful resizes so
the caller’s failure handling is triggered.

In `@server/src/Services/TypesettingService.cs`:
- Around line 151-178: Fix WhiteFillWithMask by disposing the original
SKBitmap.Decode(imagePng) result separately while retaining the copied BGRA
bitmap. Require decoded.Resize to succeed when dimensions differ; if it returns
null, return the original image instead of using a mismatched mask. Update the
unsafe pixel traversal to advance each row using bitmap.RowBytes and
mask.RowBytes, and process pixels by their actual row offsets rather than
assuming tightly packed rows.
- Line 204: Update the PNG encoding flow in RunInpaintAsync to check both the
Cv2.ImEncode success flag and encoded-buffer non-emptiness, returning the
original imagePng when encoding fails or produces no data. Correct the dilate
parameter documentation to describe it as the structuring-element size, or
change the Size(dilate, dilate) construction to use 2 * dilate + 1 when dilate
is intended as a radius.

---

Outside diff comments:
In `@server/src/Services/InpaintService.cs`:
- Around line 400-411: Remove the stale CopyRegion summary immediately before
MaskComposite, leaving only MaskComposite’s mask-compositing documentation
there. Restore that rectangular-copy summary directly above the CopyRegion
method declaration so each method has exactly its own documentation.

---

Nitpick comments:
In `@server/src/Routes/PortalRoutes.cs`:
- Around line 173-176: Update the MapDelete route for
jobs/{id}/textseg-blocks/{index} and the corresponding sibling route to
constrain the parameter as {index:int}, matching the existing bubbleIndex route
pattern. Return the parsed JSON elements directly from the handler and remove
the redundant Deserialize<object>/re-serialization step and its unused
naming-policy handling.
- Around line 137-141: Make cached JSON writes in the page translation and
block-deletion flows atomic and safe under concurrent GET requests. Reuse the
existing per-job lock when writing textseg_blocks.json, or write the complete
content to a temporary file and atomically replace the cache file; ensure
PortalRoutes’ cache read never observes partial JSON or fails due to parallel
writes.

In `@server/src/Services/PageTranslationService.cs`:
- Around line 767-778: Extract the shared centre-containment logic from
GetTypesettingBox and IsInsideAnyBubble into one helper, such as CenterInside,
and reuse it in both methods. Update LoadTextSegMaskAsync to accept a
CancellationToken and propagate the existing ct from both call sites through its
async operations.

In `@server/src/Services/TextSegmentationService.cs`:
- Around line 46-58: Update TextSegmentationService.LoadModel to create and
fully initialize a new InferenceSession and input name in local variables before
publishing them to the service fields. Dispose the existing _session before
replacing it, and assign _inputName before _session so IsReady cannot observe a
partially initialized model.
- Around line 115-122: In the crop calculation, replace the single mScale used
by the crop block with independent width and height scales derived from mW and
mH relative to ModelInput. Apply the width scale to padLeft and newW, and the
height scale to padTop and newH, preserving the existing rounding and clamping
behavior in cLeft, cTop, cW, and cH.
- Around line 94-110: Update the mask-selection logic in the segmentation output
handling to resolve the mask tensor by its ONNX output name first, rather than
assuming a positional index. Retain channel-count detection as the fallback when
names do not identify the mask, and remove the unsafe fallback that can select
lines_map; continue throwing when no usable mask output is available.
- Around line 191-214: Update BuildBinaryMask to accept the DenseTensor<float>
output from AsTensor<float>, validate that IsReversedStride is false, and read
values from Buffer.Span using the y * w + x offset instead of multidimensional
indexing. Avoid ToDenseTensor and preserve the existing thresholding and
bitmap-writing behavior.
🪄 Autofix

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: d6197559-78b3-45d9-8ac5-b977e4f8fd3c

📥 Commits

Reviewing files that changed from the base of the PR and between 1df1624 and 3ba3e96.

⛔ 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)
  • extension/package.json
  • extension/static/manifest.json
  • server/ClientApp/src/api.ts
  • server/ClientApp/src/components/BubbleCanvas.tsx
  • server/ClientApp/src/pages/StudioPage.tsx
  • server/server-csharp.csproj
  • server/src/BootExtensions.cs
  • server/src/Config.cs
  • server/src/ModelSettingsStore.cs
  • server/src/Routes/HealthRoutes.cs
  • server/src/Routes/PortalRoutes.cs
  • server/src/ServiceExtensions.cs
  • server/src/Services/InpaintService.cs
  • server/src/Services/PageTranslationService.cs
  • server/src/Services/TextSegmentationService.cs
  • server/src/Services/TypesettingService.cs
  • server/src/Workers/InferenceJob.cs
  • server/src/Workers/InferenceWorker.cs

Comment thread server/ClientApp/src/pages/StudioPage.tsx
Comment on lines +874 to +896
<div
onClick={() => setSelectedTextSegIndex(isSelected() ? null : i())}
class={`group flex cursor-pointer items-center gap-1 border-b border-slate-100 px-2 py-1.5 transition-colors ${
isSelected()
? "border-l-2 border-l-orange-400 bg-orange-50"
: "hover:bg-slate-50"
}`}
>
<span class={`font-mono text-[10px] font-medium shrink-0 ${isSelected() ? "text-orange-600" : "text-slate-400"}`}>
#{i()}
</span>
<span class="flex-1 truncate text-[10px] text-slate-500">
{box.w}×{box.h} @ {box.x},{box.y}
</span>
<button
onClick={(e) => { e.stopPropagation(); void handleDeleteTextSeg(i()); }}
class="invisible shrink-0 rounded p-0.5 text-slate-400 hover:bg-red-50 hover:text-red-500 group-hover:visible"
title="Delete TextSeg block"
aria-label="Delete TextSeg block"
>
<Trash2 class="h-3 w-3" />
</button>
</div>

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 | 🟠 Major | ⚡ Quick win

Make the TextSeg block row keyboard accessible.

The row is a div with an onClick handler. Keyboard users cannot select a block, and screen readers do not announce it as interactive. The Stage 3 overlay list at line 930 uses a <button> for the same interaction. Biome also lints a11y in this repo, as shown by the biome-ignore lint/a11y/useKeyWithMouseEvents comment in server/ClientApp/src/components/BubbleCanvas.tsx at line 514, so this row will likely fail the lint step.

Restructure the row so the selectable area is a <button> and the delete control is a sibling, not a descendant. Nesting a button inside a button is invalid HTML.

🐛 Proposed fix
-                              <div
-                                onClick={() => setSelectedTextSegIndex(isSelected() ? null : i())}
-                                class={`group flex cursor-pointer items-center gap-1 border-b border-slate-100 px-2 py-1.5 transition-colors ${
+                              <div
+                                class={`group flex items-center gap-1 border-b border-slate-100 px-2 py-1.5 transition-colors ${
                                   isSelected()
                                     ? "border-l-2 border-l-orange-400 bg-orange-50"
                                     : "hover:bg-slate-50"
                                 }`}
                               >
-                                <span class={`font-mono text-[10px] font-medium shrink-0 ${isSelected() ? "text-orange-600" : "text-slate-400"}`}>
-                                  #{i()}
-                                </span>
-                                <span class="flex-1 truncate text-[10px] text-slate-500">
-                                  {box.w}×{box.h} @ {box.x},{box.y}
-                                </span>
+                                <button
+                                  type="button"
+                                  onClick={() => setSelectedTextSegIndex(isSelected() ? null : i())}
+                                  aria-pressed={isSelected()}
+                                  class="flex min-w-0 flex-1 cursor-pointer items-center gap-1 text-left"
+                                >
+                                  <span class={`font-mono text-[10px] font-medium shrink-0 ${isSelected() ? "text-orange-600" : "text-slate-400"}`}>
+                                    #{i()}
+                                  </span>
+                                  <span class="flex-1 truncate text-[10px] text-slate-500">
+                                    {box.w}×{box.h} @ {box.x},{box.y}
+                                  </span>
+                                </button>
                                 <button
-                                  onClick={(e) => { e.stopPropagation(); void handleDeleteTextSeg(i()); }}
+                                  type="button"
+                                  onClick={() => void handleDeleteTextSeg(i())}
                                   class="invisible shrink-0 rounded p-0.5 text-slate-400 hover:bg-red-50 hover:text-red-500 group-hover:visible"
                                   title="Delete TextSeg block"
                                   aria-label="Delete TextSeg block"
                                 >
                                   <Trash2 class="h-3 w-3" />
                                 </button>
                               </div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
onClick={() => setSelectedTextSegIndex(isSelected() ? null : i())}
class={`group flex cursor-pointer items-center gap-1 border-b border-slate-100 px-2 py-1.5 transition-colors ${
isSelected()
? "border-l-2 border-l-orange-400 bg-orange-50"
: "hover:bg-slate-50"
}`}
>
<span class={`font-mono text-[10px] font-medium shrink-0 ${isSelected() ? "text-orange-600" : "text-slate-400"}`}>
#{i()}
</span>
<span class="flex-1 truncate text-[10px] text-slate-500">
{box.w}×{box.h} @ {box.x},{box.y}
</span>
<button
onClick={(e) => { e.stopPropagation(); void handleDeleteTextSeg(i()); }}
class="invisible shrink-0 rounded p-0.5 text-slate-400 hover:bg-red-50 hover:text-red-500 group-hover:visible"
title="Delete TextSeg block"
aria-label="Delete TextSeg block"
>
<Trash2 class="h-3 w-3" />
</button>
</div>
<div
class={`group flex items-center gap-1 border-b border-slate-100 px-2 py-1.5 transition-colors ${
isSelected()
? "border-l-2 border-l-orange-400 bg-orange-50"
: "hover:bg-slate-50"
}`}
>
<button
type="button"
onClick={() => setSelectedTextSegIndex(isSelected() ? null : i())}
aria-pressed={isSelected()}
class="flex min-w-0 flex-1 cursor-pointer items-center gap-1 text-left"
>
<span class={`font-mono text-[10px] font-medium shrink-0 ${isSelected() ? "text-orange-600" : "text-slate-400"}`}>
#{i()}
</span>
<span class="flex-1 truncate text-[10px] text-slate-500">
{box.w}×{box.h} @ {box.x},{box.y}
</span>
</button>
<button
type="button"
onClick={() => void handleDeleteTextSeg(i())}
class="invisible shrink-0 rounded p-0.5 text-slate-400 hover:bg-red-50 hover:text-red-500 group-hover:visible"
title="Delete TextSeg block"
aria-label="Delete TextSeg block"
>
<Trash2 class="h-3 w-3" />
</button>
</div>
🤖 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 874 - 896,
Restructure the TextSeg row around the existing selection handler so the
selectable area is a button, making it keyboard-accessible and announced as
interactive. Move the delete button currently inside the row to be a sibling
outside the selectable button, while preserving event propagation prevention,
styling, and selection behavior without nesting buttons.

Comment thread server/ClientApp/src/pages/StudioPage.tsx Outdated
Comment on lines +36 to +37
<PackageReference Include="OpenCvSharp4" Version="4.10.0.20241108" />
<PackageReference Include="OpenCvSharp4.official.runtime.linux-x64" Version="4.13.0.20260627" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check published OpenCvSharp package versions and find usage in the repository.
set -uo pipefail

for pkg in opencvsharp4 opencvsharp4.official.runtime.linux-x64; do
  echo "=== $pkg ==="
  curl -s "https://api.nuget.org/v3-flatcontainer/${pkg}/index.json" | jq -r '.versions[-15:][]'
done

# Where is OpenCV actually used?
rg -n --type=cs -C3 'OpenCvSharp|Cv2\.'

Repository: deckyfx/web-ocr

Length of output: 702


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== project file ==="
cat -n server/server-csharp.csproj | sed -n '25,45p'

echo "=== C# files and OpenCvSharp references ==="
git ls-files '*.cs' | while IFS= read -r f; do
  if rg -n -C3 'OpenCvSharp|Cv2\.' "$f"; then
    :
  fi
done

echo "=== package metadata ==="
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
for spec in \
  "opencvsharp4/4.10.0.20241108" \
  "opencvsharp4/4.13.0.20260627" \
  "opencvsharp4.official.runtime.linux-x64/4.13.0.20260627"
do
  pkg="${spec%/*}"
  ver="${spec#*/}"
  url="https://api.nuget.org/v3-flatcontainer/${pkg}/${ver}/${pkg}.${ver}.nupkg"
  out="$tmpdir/${pkg}.${ver}.nupkg"
  echo "--- $pkg $ver ---"
  curl -fsSL "$url" -o "$out"
  unzip -l "$out" | sed -n '1,80p'
  unzip -p "$out" '*.nuspec' | sed -n '1,160p'
done

Repository: deckyfx/web-ocr

Length of output: 13863


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== target framework and runtime identifiers ==="
cat -n server/server-csharp.csproj | sed -n '1,28p'

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

download_pkg() {
  local pkg="$1" ver="$2"
  local lower_pkg="${pkg,,}"
  local out="$tmpdir/${lower_pkg}.${ver}.nupkg"
  curl -fsSL "https://api.nuget.org/v3-flatcontainer/${lower_pkg}/${ver}/${lower_pkg}.${ver}.nupkg" -o "$out"
  mkdir -p "$tmpdir/${lower_pkg}.${ver}"
  unzip -q "$out" -d "$tmpdir/${lower_pkg}.${ver}"
}

echo "=== available 4.10 runtime versions ==="
curl -fsSL 'https://api.nuget.org/v3-flatcontainer/opencvsharp4.official.runtime.linux-x64/index.json' |
  jq -r '.versions[]' | grep '^4\.10\.'

download_pkg 'OpenCvSharp4.official.runtime.linux-x64' '4.10.0.20241108'
download_pkg 'OpenCvSharp4.official.runtime.linux-x64' '4.13.0.20260627'

echo "=== native library exports: matching 4.10 versus selected 4.13 ==="
for f in \
  "$tmpdir"/opencvsharp4.official.runtime.linux-x64.4.10.0.20241108/runtimes/linux-x64/native/libOpenCvSharpExtern.so \
  "$tmpdir"/opencvsharp4.official.runtime.linux-x64.4.13.0.20260627/runtimes/linux-x64/native/libOpenCvSharpExtern.so
do
  echo "--- $f ---"
  file "$f"
  nm -D --defined-only "$f" | awk '{print $3}' | sort > "$f.exports"
  echo "export count: $(wc -l < "$f.exports")"
done

comm -3 \
  "$tmpdir"/opencvsharp4.official.runtime.linux-x64.4.10.0.20241108/runtimes/linux-x64/native/libOpenCvSharpExtern.so.exports \
  "$tmpdir"/opencvsharp4.official.runtime.linux-x64.4.13.0.20260627/runtimes/linux-x64/native/libOpenCvSharpExtern.so.exports |
  sed -n '1,120p'

echo "=== symbols used by the 4.10 managed assembly for this code path ==="
download_pkg 'OpenCvSharp4' '4.10.0.20241108'
strings "$tmpdir"/opencvsharp4.4.10.0.20241108/lib/net6.0/OpenCvSharp.dll |
  grep -E 'core_.*(imdecode|rectangle|inpaint|imencode|countNonZero|dilate|getStructuringElement)|OpenCvSharpExtern' |
  sort -u

Repository: deckyfx/web-ocr

Length of output: 2033


🏁 Script executed:

#!/bin/bash
set -u

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

download_pkg() {
  local pkg="$1" ver="$2"
  local lower_pkg="${pkg,,}"
  local out="$tmpdir/${lower_pkg}.${ver}.nupkg"
  curl -fsSL "https://api.nuget.org/v3-flatcontainer/${lower_pkg}/${ver}/${lower_pkg}.${ver}.nupkg" -o "$out" || return 1
  mkdir -p "$tmpdir/${lower_pkg}.${ver}"
  unzip -q "$out" -d "$tmpdir/${lower_pkg}.${ver}" || return 1
}

download_pkg 'OpenCvSharp4.official.runtime.linux-x64' '4.10.0.20241108'
download_pkg 'OpenCvSharp4.official.runtime.linux-x64' '4.13.0.20260627'
download_pkg 'OpenCvSharp4' '4.10.0.20241108'

echo "=== native export comparison ==="
for ver in 4.10.0.20241108 4.13.0.20260627; do
  f="$tmpdir/opencvsharp4.official.runtime.linux-x64.$ver/runtimes/linux-x64/native/libOpenCvSharpExtern.so"
  echo "--- $ver ---"
  if command -v nm >/dev/null 2>&1; then
    nm -D --defined-only "$f" | awk '{print $3}' | sort -u > "$tmpdir/$ver.exports"
  else
    readelf -Ws "$f" | awk '$4 == "FUNC" && $7 != "UND" {print $8}' | sort -u > "$tmpdir/$ver.exports"
  fi
  wc -l "$tmpdir/$ver.exports"
done
echo "--- symbols only in one version (first 160) ---"
comm -3 "$tmpdir/4.10.0.20241108.exports" "$tmpdir/4.13.0.20260627.exports" | sed -n '1,160p'

echo "=== managed wrapper strings relevant to the repository call path ==="
strings "$tmpdir/opencvsharp4.4.10.0.20241108/lib/net6.0/OpenCvSharp.dll" |
  grep -Ei 'OpenCvSharpExtern|inpaint|imdecode|imencode|rectangle|countnonzero|dilate|getstructuringelement' |
  sort -u | sed -n '1,160p'

Repository: deckyfx/web-ocr

Length of output: 4029


🏁 Script executed:

#!/bin/bash
set -u

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

download_pkg() {
  local pkg="$1" ver="$2"
  local id="${pkg,,}"
  local nupkg="$tmpdir/$id.$ver.nupkg"
  curl -fsSL "https://api.nuget.org/v3-flatcontainer/$id/$ver/$id.$ver.nupkg" -o "$nupkg" || return 1
  mkdir -p "$tmpdir/$id.$ver"
  unzip -q "$nupkg" -d "$tmpdir/$id.$ver" || return 1
}

download_pkg opencvsharp4 4.10.0.20241108
download_pkg opencvsharp4.official.runtime.linux-x64 4.10.0.20241108
download_pkg opencvsharp4.official.runtime.linux-x64 4.13.0.20260627

echo "=== runtime README and package references ==="
for f in \
  "$tmpdir"/opencvsharp4.official.runtime.linux-x64.4.10.0.20241108/README.runtime.md \
  "$tmpdir"/opencvsharp4.official.runtime.linux-x64.4.13.0.20260627/README.runtime.md
do
  echo "--- $f ---"
  sed -n '1,180p' "$f"
done

echo "=== OpenCvSharp bridge symbols in native libraries ==="
for ver in 4.10.0.20241108 4.13.0.20260627; do
  f="$tmpdir/opencvsharp4.official.runtime.linux-x64.$ver/runtimes/linux-x64/native/libOpenCvSharpExtern.so"
  echo "--- $ver ---"
  nm -D --defined-only "$f" 2>/dev/null |
    awk '{print $3}' |
    grep -E '^(core|imgcodecs|imgproc|photo)_' |
    grep -Ei 'inpaint|imdecode|imencode|rectangle|count.?non.?zero|dilate|getstructuringelement' |
    sort -u
done

echo "=== relevant managed assembly strings ==="
python3 - "$tmpdir/opencvsharp4.4.10.0.20241108/lib/net6.0/OpenCvSharp.dll" <<'PY'
import re, sys
data = open(sys.argv[1], "rb").read()
strings = sorted(set(x.decode("ascii", "ignore") for x in re.findall(rb"[\x20-\x7e]{4,}", data)))
for s in strings:
    if ("OpenCvSharpExtern" in s or
        any(k in s.lower() for k in ("inpaint", "imdecode", "imencode", "rectangle", "countnonzero", "dilate", "getstructuringelement"))):
        print(s)
PY

echo "=== repository platform/deployment references ==="
git ls-files | grep -E '(^|/)(README|Dockerfile|.*\.yml$|.*\.yaml$|.*\.csproj$)' |
  while IFS= read -r f; do
    rg -n -i -C2 'linux|windows|macos|macOS|runtime|platform|decky|steam deck' "$f" || true
  done

Repository: deckyfx/web-ocr

Length of output: 6857


🏁 Script executed:

#!/bin/bash
set -u

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

curl -fsSL \
  'https://api.nuget.org/v3-flatcontainer/opencvsharp4/4.10.0.20241108/opencvsharp4.10.0.20241108.nupkg' |
  unzip -p - 'lib/net6.0/OpenCvSharp.dll' > "$tmpdir/OpenCvSharp.dll"

echo "=== managed assembly strings for the inpainting call path ==="
python3 - "$tmpdir/OpenCvSharp.dll" <<'PY'
import re, sys
data = open(sys.argv[1], "rb").read()
items = sorted(set(x.decode("ascii", "ignore")
                   for x in re.findall(rb"[\x20-\x7e]{4,}", data)))
needles = ("OpenCvSharpExtern", "inpaint", "imdecode", "imencode",
           "rectangle", "countnonzero", "dilate", "getstructuringelement")
for item in items:
    if any(needle in item.lower() for needle in needles):
        print(item)
PY

echo "=== repository files and platform statements ==="
git ls-files | sed -n '1,120p'
rg -n -i -C2 'linux|windows|macos|macOS|steam deck|decky|platform|deployment|supported' \
  --glob '!server/server-csharp.csproj' . || true

Repository: deckyfx/web-ocr

Length of output: 50374


Add native runtimes for supported operating systems.

The server’s documented targets include Windows and macOS, but this project references only the Linux x64 runtime. Add the required Windows and macOS runtime packages so OpenCV calls do not fail on those platforms.

🤖 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/server-csharp.csproj` around lines 36 - 37, Add the OpenCvSharp native
runtime package references for the supported Windows and macOS platforms
alongside OpenCvSharp4 and the existing Linux x64 runtime in the project
configuration. Use the appropriate Windows and macOS runtime package variants
while preserving the current Linux runtime reference.

Comment thread server/src/BootExtensions.cs
Comment thread server/src/Services/PageTranslationService.cs Outdated
Comment thread server/src/Services/PageTranslationService.cs Outdated
Comment thread server/src/Services/TextSegmentationService.cs
Comment thread server/src/Services/TypesettingService.cs
Comment thread server/src/Services/TypesettingService.cs Outdated
Comments 1-3 (StudioPage.tsx):
- Surface TextSeg load failure via setActionError in catch block
- Replace div row with button for keyboard accessibility
- Add group-focus-within:visible / focus-visible:visible to delete button

Comment 4 (server-csharp.csproj):
- Add OpenCvSharp4.runtime.win for Windows cross-platform support
  (macOS arm64 package not yet published by schimatk)

Comment 5 (BootExtensions.cs):
- Wrap DownloadTextSegAsync body in try/catch so a download failure
  is non-fatal (logged as warning, server continues without TextSeg)

Comments 8-9 (PageTranslationService.cs):
- Group TextSeg blocks by typesetting target bubble before OCR/translate
  using BubbleBoxComparer to prevent duplicate overlapping translations
- Add LoadCachedTextSegAsync that loads both mask PNG + textseg_blocks.json
  so Telea pass receives block list on re-render/re-inpaint paths

Comments 10-13 (various services):
- Guard LaMa path on empty TextSeg mask (preSeg?.Mask is {Length>0})
- Throw InvalidOperationException on null resize in TextSeg BuildLetterbox
- Fix WhiteFillWithMask bitmap leak + unsafe RowBytes pointer walk
- Check Cv2.ImEncode return value in TeleaInpaintBlocks; fix doc comment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@deckyfx
deckyfx merged commit 193cc47 into master Aug 11, 2026
1 check passed
@deckyfx
deckyfx deleted the feat/text-seg-inpaint branch August 11, 2026 17:54
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