Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion extension/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "selfhost-ocr-extension",
"version": "1.3.6",
"version": "1.3.7",
"private": true,
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion extension/static/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Selfhost OCR",
"version": "1.3.6",
"version": "1.3.7",
"description": "OCR screen regions using Tesseract.js or your self-hosted server.",
"permissions": [
"activeTab",
Expand Down
14 changes: 14 additions & 0 deletions server/ClientApp/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,3 +339,17 @@ export function jobInpaintedUrl(id: string): string {
export function jobResultUrl(id: string): string {
return `/api/portal/jobs/${id}/result`;
}

export type TextSegBox = { x: number; y: number; w: number; h: number };

export async function getJobTextSegBlocks(id: string): Promise<TextSegBox[]> {
const r = await fetch(`/api/portal/jobs/${id}/textseg-blocks`);
if (!r.ok) throw new Error(await r.text());
return r.json() as Promise<TextSegBox[]>;
}

export async function deleteTextSegBlock(id: string, index: number): Promise<TextSegBox[]> {
const r = await fetch(`/api/portal/jobs/${id}/textseg-blocks/${index}`, { method: "DELETE" });
if (!r.ok) throw new Error(await r.text());
return r.json() as Promise<TextSegBox[]>;
}
37 changes: 37 additions & 0 deletions server/ClientApp/src/components/BubbleCanvas.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createSignal, onMount, onCleanup, For, Show } from "solid-js";
import type { JSX } from "solid-js";
import type { TranslationBubble } from "../types";
import type { TextSegBox } from "../api";

// ---------------------------------------------------------------------------
// Types
Expand Down Expand Up @@ -82,6 +83,16 @@ export interface BubbleCanvasProps {
onDraw: (x: number, y: number, w: number, h: number) => void;
/** Called when the rotation handle is dragged; degrees, not normalised. Stage 3 only. */
onRotate?: (bubbleIndex: number, rotation: number) => void;
/**
* Optional TextSeg text-block boxes to show as a read-only dashed orange
* overlay. Rendered beneath the editable bubble rects so they don't
* interfere with pointer events.
*/
overlayBoxes?: TextSegBox[];
/** Index into overlayBoxes that is currently selected (highlighted in the sidebar). */
selectedTextSegIndex?: number | null;
/** When false, bubble bounding boxes are hidden. Default true. */
showBubbles?: boolean;
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -447,6 +458,31 @@ export function BubbleCanvas(props: BubbleCanvasProps): JSX.Element {
style={{ cursor: svgCursor() }}
onMouseDown={handleSvgMouseDown}
>
{/* TextSeg text-block overlay — dashed orange, non-interactive */}
<Show when={(props.overlayBoxes?.length ?? 0) > 0}>
<For each={props.overlayBoxes}>
{(box, i) => {
const tl = () => toSvg(box.x, box.y);
const br = () => toSvg(box.x + box.w, box.y + box.h);
const isSelected = () => props.selectedTextSegIndex === i();
return (
<rect
x={tl().x}
y={tl().y}
width={Math.max(0, br().x - tl().x)}
height={Math.max(0, br().y - tl().y)}
fill={isSelected() ? "rgba(255,120,0,0.18)" : "rgba(255,120,0,0.07)"}
stroke={isSelected() ? "#ea6800" : "#f97316"}
stroke-width={isSelected() ? "2.5" : "1.5"}
stroke-dasharray="5,3"
style={{ "pointer-events": "none" }}
/>
);
}}
</For>
</Show>

<Show when={props.showBubbles !== false}>
<For each={props.bubbles}>
{(bubble) => {
const svgRect = () => computeSvgRect(bubble);
Expand Down Expand Up @@ -655,6 +691,7 @@ export function BubbleCanvas(props: BubbleCanvasProps): JSX.Element {
);
}}
</For>
</Show>

{/* Draw-mode preview rect */}
<Show when={getDrawPreview()}>
Expand Down
133 changes: 131 additions & 2 deletions server/ClientApp/src/pages/StudioPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import {
addBubble,
deleteBubble,
deleteJob,
deleteTextSegBlock,
getJob,
getJobBubbles,
getJobTextSegBlocks,
inpaintJob,
jobInpaintedUrl,
jobOriginalUrl,
Expand All @@ -37,7 +39,7 @@ import {
translateJob,
updateBubble,
} from "../api";
import type { UpdateBubbleBody } from "../api";
import type { TextSegBox, UpdateBubbleBody } from "../api";
import { BubbleCanvas } from "../components/BubbleCanvas";
import { BubbleList } from "../components/BubbleList";
import { BubbleEditor } from "../components/BubbleEditor";
Expand Down Expand Up @@ -112,6 +114,45 @@ export function StudioPage() {
);
const [imageVersion, setImageVersion] = createSignal(0);

// TextSeg overlay
const [showTextSeg, setShowTextSeg] = createSignal(false);
const [textSegBoxes, setTextSegBoxes] = createSignal<TextSegBox[]>([]);
const [isLoadingTextSeg, setIsLoadingTextSeg] = createSignal(false);
const [selectedTextSegIndex, setSelectedTextSegIndex] = createSignal<number | null>(null);

// Bubble visibility toggle
const [showBubbles, setShowBubbles] = createSignal(true);

async function handleToggleTextSeg(): Promise<void> {
const next = !showTextSeg();
setShowTextSeg(next);
if (!next) { setSelectedTextSegIndex(null); return; }
if (textSegBoxes().length === 0) {
setIsLoadingTextSeg(true);
try {
const boxes = await getJobTextSegBlocks(params.id);
setTextSegBoxes(boxes);
} catch (err) {
setShowTextSeg(false);
setActionError(err instanceof Error ? err.message : "Failed to load TextSeg blocks");
} finally {
setIsLoadingTextSeg(false);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

async function handleDeleteTextSeg(index: number): Promise<void> {
try {
const updated = await deleteTextSegBlock(params.id, index);
setTextSegBoxes(updated);
if (selectedTextSegIndex() === index) setSelectedTextSegIndex(null);
else if ((selectedTextSegIndex() ?? 0) > index)
setSelectedTextSegIndex((v) => (v ?? 0) - 1);
} catch (err) {
setActionError(err instanceof Error ? err.message : "Failed to delete TextSeg block");
}
}

// Derived
const bubbleList = () => bubbles() ?? [];
const selectedBubble = (): TranslationBubble | null => {
Expand Down Expand Up @@ -421,6 +462,9 @@ export function StudioPage() {
selectedIndex: selectedIndex(),
drawMode: isDrawMode(),
bubblePadding: bubblePadding(),
overlayBoxes: showTextSeg() ? textSegBoxes() : undefined,
selectedTextSegIndex: showTextSeg() ? selectedTextSegIndex() : null,
showBubbles: showBubbles(),
onSelect: handleSelectStage1,
onMove: handleMove,
onResize: handleResize,
Expand Down Expand Up @@ -499,6 +543,7 @@ export function StudioPage() {
return j.inpaintedImagePath ? (
<BubbleCanvas
{...readOnlyCanvasProps()}
showBubbles={showBubbles()}
imageUrl={inpaintedUrl}
imageWidth={j.originalWidth}
imageHeight={j.originalHeight}
Expand Down Expand Up @@ -611,6 +656,41 @@ export function StudioPage() {
>+</button>
</div>

{/* TextSeg overlay toggle */}
<button
onClick={handleToggleTextSeg}
disabled={isLoadingTextSeg()}
aria-pressed={showTextSeg()}
title="Toggle TextSeg text-block overlay (orange dashed = OCR/inpaint regions)"
class={`flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${
showTextSeg()
? "border-orange-300 bg-orange-50 text-orange-700 hover:bg-orange-100"
: "border-slate-200 bg-white text-slate-600 hover:bg-slate-50"
}`}
>
<Show when={isLoadingTextSeg()} fallback={
<span class="inline-block h-2.5 w-2.5 rounded-sm border-2 border-current" style={{ "border-style": "dashed" }} />
}>
<RefreshCw class="h-3.5 w-3.5 animate-spin" />
</Show>
TextSeg
</button>

{/* Bubble box visibility toggle */}
<button
onClick={() => setShowBubbles((v) => !v)}
aria-pressed={showBubbles()}
title="Toggle bubble bounding-box overlay (blue = detection boxes)"
class={`flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors ${
showBubbles()
? "border-blue-300 bg-blue-50 text-blue-700 hover:bg-blue-100"
: "border-slate-200 bg-white text-slate-400 hover:bg-slate-50"
}`}
>
<span class="inline-block h-2.5 w-2.5 rounded-sm border-2 border-current" />
Bubbles
</button>

{/* Separator */}
<div class="mx-1 h-5 w-px bg-slate-200" />

Expand Down Expand Up @@ -766,7 +846,9 @@ export function StudioPage() {
<Show when={stage1Active()}>
<div
class={`flex flex-col overflow-hidden ${
stage3Active() ? "flex-1 border-b border-slate-200" : "flex-1"
(stage3Active() || (showTextSeg() && textSegBoxes().length > 0))
? "flex-[1] border-b border-slate-200"
: "flex-1"
}`}
>
<BubbleList
Expand All @@ -776,6 +858,53 @@ export function StudioPage() {
onAddBubble={() => setIsDrawMode(true)}
/>
</div>

{/* TextSeg block management list */}
<Show when={showTextSeg() && textSegBoxes().length > 0}>
<div class={`flex flex-[1] flex-col overflow-hidden ${stage3Active() ? "border-b border-slate-200" : ""}`}>
<div class="flex shrink-0 items-center gap-1 border-b border-slate-100 px-3 py-2">
<span class="flex-1 text-xs font-semibold uppercase tracking-wide text-orange-600">
TextSeg ({textSegBoxes().length})
</span>
</div>
<div class="flex-1 overflow-y-auto">
<For each={textSegBoxes()}>
{(box, i) => {
const isSelected = () => selectedTextSegIndex() === i();
return (
<button
type="button"
onClick={() => setSelectedTextSegIndex(isSelected() ? null : i())}
class={`group flex w-full cursor-pointer items-center gap-1 border-b border-slate-100 px-2 py-1.5 text-left 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>
<span
role="button"
tabIndex={0}
onClick={(e) => { e.stopPropagation(); void handleDeleteTextSeg(i()); }}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { 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 group-focus-within:visible focus-visible:visible"
title="Delete TextSeg block"
aria-label="Delete TextSeg block"
>
<Trash2 class="h-3 w-3" />
</span>
</button>
);
}}
</For>
</div>
</div>
</Show>
</Show>

{/* Stage 3 section: text overlay list */}
Expand Down
4 changes: 4 additions & 0 deletions server/server-csharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
<PackageReference Include="LibNMeCab.IpaDicBin" Version="0.10.*" />
<PackageReference Include="SkiaSharp" Version="3.119.4" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
<PackageReference Include="OpenCvSharp4" Version="4.10.0.20241108" />
<PackageReference Include="OpenCvSharp4.official.runtime.linux-x64" Version="4.13.0.20260627" />
Comment on lines +36 to +37

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.

<!-- Windows native binaries (x64); no matching macOS arm64 package exists yet) -->
<PackageReference Include="OpenCvSharp4.runtime.win" Version="4.10.0.20241108" />
</ItemGroup>

</Project>
Loading