Rewrite oemer U-Net ConvTranspose pads before packaging.
AppImage release / appimage (push) Successful in 1m42s

ONNX Runtime 1.30+ rejects pads [0, 0, -1, -1] on the unet checkpoint. The build now rewrites those nodes to pads [0, 0, 0, 0] and output_padding [1, 1], which keeps the same output size. Version 0.1.1.
This commit is contained in:
Cursor Agent committed 2026-10-11 07:51:24 +00:00
1 parent ed55f02983
commit 6da116518f
5 files changed
+160 -10

No files matched your search

+6 -4
View File
@@ -7,14 +7,14 @@ The usual way to run it is one Linux AppImage. That file contains the window, th
## Run the AppImage ## Run the AppImage
```bash ```bash
chmod +x Recorder-Tab-Converter-0.1.0.AppImage chmod +x Recorder-Tab-Converter-0.1.1.AppImage
./Recorder-Tab-Converter-0.1.0.AppImage ./Recorder-Tab-Converter-0.1.1.AppImage
``` ```
If the file will not start because FUSE is missing: If the file will not start because FUSE is missing:
```bash ```bash
APPIMAGE_EXTRACT_AND_RUN=1 ./Recorder-Tab-Converter-0.1.0.AppImage APPIMAGE_EXTRACT_AND_RUN=1 ./Recorder-Tab-Converter-0.1.1.AppImage
``` ```
Open **Twinkle, Twinkle, Little Star** or **Ode to Joy** to see tabs immediately. Or drop a PNG, JPG, or WEBP of a page. The note list under the tab is editable. Fix a bad pitch or duration and the fingerings update. If a photo cannot be read, the app says so and does not invent notes. Open **Twinkle, Twinkle, Little Star** or **Ode to Joy** to see tabs immediately. Or drop a PNG, JPG, or WEBP of a page. The note list under the tab is editable. Fix a bad pitch or duration and the fingerings update. If a photo cannot be read, the app says so and does not invent notes.
@@ -30,9 +30,11 @@ From a checkout, one command:
That installs Node packages, downloads a relocatable Python plus oemer and its ONNX models, builds the app, and writes: That installs Node packages, downloads a relocatable Python plus oemer and its ONNX models, builds the app, and writes:
```text ```text
dist/Recorder-Tab-Converter-0.1.0.AppImage dist/Recorder-Tab-Converter-0.1.1.AppImage
``` ```
The U-Net checkpoint from oemer 0.1.8 has ConvTranspose pads `[0, 0, -1, -1]`. The build rewrites those nodes to pads `[0, 0, 0, 0]` and `output_padding` `[1, 1]` before packaging. That is the same output size, and it is what current ONNX Runtime will load. The rewrite does not change fingerings.
The build needs network access the first time. The AppImage is not committed; keep it under 600MB. The build needs network access the first time. The AppImage is not committed; keep it under 600MB.
## Develop in a browser ## Develop in a browser
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "recorder-tab-converter", "name": "recorder-tab-converter",
"version": "0.1.0", "version": "0.1.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "recorder-tab-converter", "name": "recorder-tab-converter",
"version": "0.1.0", "version": "0.1.1",
"dependencies": { "dependencies": {
"@base-ui/react": "^1.8.0", "@base-ui/react": "^1.8.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "recorder-tab-converter", "name": "recorder-tab-converter",
"version": "0.1.0", "version": "0.1.1",
"private": true, "private": true,
"description": "Turn a sheet-music image into soprano recorder fingering tabs, with rhythm kept.", "description": "Turn a sheet-music image into soprano recorder fingering tabs, with rhythm kept.",
"main": "electron/main.cjs", "main": "electron/main.cjs",
+36 -3
View File
@@ -7,12 +7,13 @@ ROOT="$(pwd)"
echo "== Install Node packages ==" echo "== Install Node packages =="
npm install npm install
PY="$ROOT/build/python/bin/python3"
if ! "$PY" -c "import oemer.ete, onnxruntime" >/dev/null 2>&1; then
echo "== Download a relocatable Python and oemer =="
mkdir -p "$ROOT/build/cache" mkdir -p "$ROOT/build/cache"
TARBALL="$ROOT/build/cache/cpython.tar.gz" TARBALL="$ROOT/build/cache/cpython.tar.gz"
URL="https://github.com/astral-sh/python-build-standalone/releases/download/20261003/cpython-3.12.15%2B20261003-x86_64-unknown-linux-gnu-install_only.tar.gz" URL="https://github.com/astral-sh/python-build-standalone/releases/download/20261003/cpython-3.12.15%2B20261003-x86_64-unknown-linux-gnu-install_only.tar.gz"
PY="$ROOT/build/python/bin/python3"
if ! "$PY" -c "import oemer.ete, onnxruntime" >/dev/null 2>&1; then
echo "== Download a relocatable Python and oemer =="
if [[ ! -f "$TARBALL" ]]; then if [[ ! -f "$TARBALL" ]]; then
curl -fL --retry 3 -o "$TARBALL" "$URL" curl -fL --retry 3 -o "$TARBALL" "$URL"
fi fi
@@ -36,6 +37,38 @@ if [[ ! -f "$SITE/checkpoints/seg_net/model.onnx" ]]; then
curl -fL --retry 3 -o "$SITE/checkpoints/seg_net/model.onnx" \ curl -fL --retry 3 -o "$SITE/checkpoints/seg_net/model.onnx" \
"https://github.com/BreezeWhite/oemer/releases/download/checkpoints/2nd_model.onnx" "https://github.com/BreezeWhite/oemer/releases/download/checkpoints/2nd_model.onnx"
fi fi
echo "== Rewrite U-Net ConvTranspose pads =="
# oemer 0.1.8's unet has ConvTranspose pads [0, 0, -1, -1]. ONNX Runtime
# 1.30+ rejects those. The rewrite keeps the same output size. onnx itself
# is not packaged.
if ! "$PY" -m pip --version >/dev/null 2>&1; then
if [[ ! -f "$TARBALL" ]]; then
echo "pip was removed and the Python tarball is missing, so the U-Net cannot be rewritten." >&2
exit 1
fi
PIP_RESTORE="$ROOT/build/cache/pip-restore"
rm -rf "$PIP_RESTORE"
mkdir -p "$PIP_RESTORE"
tar -xzf "$TARBALL" -C "$PIP_RESTORE" --wildcards \
'python/lib/python3.12/site-packages/pip' \
'python/lib/python3.12/site-packages/pip-*.dist-info'
rm -rf "$ROOT/build/python/lib/python3.12/site-packages/pip" \
"$ROOT/build/python/lib/python3.12/site-packages/pip-"*.dist-info
cp -a "$PIP_RESTORE/python/lib/python3.12/site-packages/pip" \
"$ROOT/build/python/lib/python3.12/site-packages/"
cp -a "$PIP_RESTORE"/python/lib/python3.12/site-packages/pip-*.dist-info \
"$ROOT/build/python/lib/python3.12/site-packages/"
rm -rf "$PIP_RESTORE"
fi
PATCH_PY="$ROOT/build/cache/onnx-patch"
rm -rf "$PATCH_PY"
"$PY" -m pip install --disable-pip-version-check --target "$PATCH_PY" 'onnx>=1.16,<2'
PYTHONPATH="$PATCH_PY${PYTHONPATH:+:$PYTHONPATH}" \
"$PY" "$ROOT/scripts/patch_oemer_unet.py" \
"$SITE/checkpoints/unet_big/model.onnx"
rm -rf "$PATCH_PY"
MPLBACKEND=Agg "$PY" -c "import oemer.ete, cv2; from cv2.typing import RotatedRect; print('oemer', cv2.__version__)" MPLBACKEND=Agg "$PY" -c "import oemer.ete, cv2; from cv2.typing import RotatedRect; print('oemer', cv2.__version__)"
echo "== Trim the Python prefix ==" echo "== Trim the Python prefix =="
+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""Rewrite oemer U-Net ConvTranspose pads so ONNX Runtime can load the model.
oemer 0.1.8's 1st_model.onnx ships ConvTranspose nodes with pads
[0, 0, -1, -1]. ONNX Runtime 1.30 and newer reject those during shape
inference. pads [0, 0, 0, 0] with output_padding [1, 1] is the same
output size for these stride-2, kernel-1 nodes. Weights are left as they are.
"""
from __future__ import annotations
import sys
from pathlib import Path
import onnx
from onnx import helper
BAD_PADS = [0, 0, -1, -1]
FIXED_PADS = [0, 0, 0, 0]
FIXED_OUTPUT_PADDING = [1, 1]
CRASH_NODE = "model/conv2d_transpose_1/conv2d_transpose"
def _attr(node: onnx.NodeProto, name: str) -> onnx.AttributeProto | None:
for attr in node.attribute:
if attr.name == name:
return attr
return None
def _ints(node: onnx.NodeProto, name: str) -> list[int] | None:
attr = _attr(node, name)
if attr is None:
return None
return list(attr.ints)
def _set_ints(attr: onnx.AttributeProto, values: list[int]) -> None:
del attr.ints[:]
attr.ints.extend(values)
def rewrite(model_path: Path) -> int:
model = onnx.load(model_path)
changed = 0
for node in model.graph.node:
if node.op_type != "ConvTranspose":
continue
pads = _ints(node, "pads")
if pads is None or pads == FIXED_PADS:
continue
if pads != BAD_PADS:
if any(value < 0 for value in pads):
raise SystemExit(
f"{model_path}: {node.name} has unexpected negative pads {pads}"
)
continue
pads_attr = _attr(node, "pads")
if pads_attr is None:
raise SystemExit(f"{model_path}: {node.name} is missing pads")
_set_ints(pads_attr, FIXED_PADS)
output_padding = _attr(node, "output_padding")
if output_padding is None:
node.attribute.append(
helper.make_attribute("output_padding", FIXED_OUTPUT_PADDING)
)
else:
_set_ints(output_padding, FIXED_OUTPUT_PADDING)
changed += 1
print(
f"rewrote {node.name}: pads {FIXED_PADS} output_padding {FIXED_OUTPUT_PADDING}"
)
if changed:
onnx.save(model, model_path)
verify(model_path)
print(f"rewrote {changed} ConvTranspose node(s) in {model_path}")
return changed
def verify(model_path: Path) -> None:
model = onnx.load(model_path)
found = False
for node in model.graph.node:
if node.op_type != "ConvTranspose":
continue
pads = _ints(node, "pads") or []
if any(value < 0 for value in pads):
raise SystemExit(f"{model_path}: {node.name} still has negative pads {pads}")
if node.name != CRASH_NODE:
continue
found = True
output_padding = _ints(node, "output_padding")
if pads != FIXED_PADS or output_padding != FIXED_OUTPUT_PADDING:
raise SystemExit(
f"{model_path}: {node.name} is {pads} / {output_padding}, "
f"expected {FIXED_PADS} / {FIXED_OUTPUT_PADDING}"
)
if not found:
raise SystemExit(f"{model_path}: missing {CRASH_NODE}")
def main() -> int:
if len(sys.argv) != 2:
print("usage: patch_oemer_unet.py MODEL.onnx", file=sys.stderr)
return 2
path = Path(sys.argv[1])
if not path.is_file():
print(f"missing model: {path}", file=sys.stderr)
return 1
rewrite(path)
return 0
if __name__ == "__main__":
raise SystemExit(main())