1 Commits
Author SHA1 Message Date
Cursor Agent 6da116518f 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.
2026-10-11 07:51:24 +00:00
5 changed files with 160 additions and 10 deletions

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
```bash
chmod +x Recorder-Tab-Converter-0.1.0.AppImage
./Recorder-Tab-Converter-0.1.0.AppImage
chmod +x Recorder-Tab-Converter-0.1.1.AppImage
./Recorder-Tab-Converter-0.1.1.AppImage
```
If the file will not start because FUSE is missing:
```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.
@@ -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:
```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.
## Develop in a browser
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "recorder-tab-converter",
"version": "0.1.0",
"version": "0.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "recorder-tab-converter",
"version": "0.1.0",
"version": "0.1.1",
"dependencies": {
"@base-ui/react": "^1.8.0",
"class-variance-authority": "^0.7.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "recorder-tab-converter",
"version": "0.1.0",
"version": "0.1.1",
"private": true,
"description": "Turn a sheet-music image into soprano recorder fingering tabs, with rhythm kept.",
"main": "electron/main.cjs",
+36 -3
View File
@@ -7,12 +7,13 @@ ROOT="$(pwd)"
echo "== Install Node packages =="
npm install
mkdir -p "$ROOT/build/cache"
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"
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"
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"
if [[ ! -f "$TARBALL" ]]; then
curl -fL --retry 3 -o "$TARBALL" "$URL"
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" \
"https://github.com/BreezeWhite/oemer/releases/download/checkpoints/2nd_model.onnx"
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__)"
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())