Add a soprano recorder tab converter as a Linux AppImage.
The app turns a lead line into Baroque fingering tabs with rhythm kept, ships Twinkle and Ode to Joy, and reads photos locally with oemer when the bundled Python is present. Co-authored-by: catpersonisme <catpersonisme@gmail.com>
This commit is contained in:
55 files changed
+17536
No files matched your search
Executable
+97
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
# One-command AppImage build. Needs network the first time (Python, models, Electron).
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
ROOT="$(pwd)"
|
||||
|
||||
echo "== Install Node packages =="
|
||||
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"
|
||||
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
|
||||
rm -rf "$ROOT/build/python"
|
||||
tar -xzf "$TARBALL" -C "$ROOT/build"
|
||||
"$PY" -m pip install --upgrade pip
|
||||
"$PY" -m pip install \
|
||||
"opencv-python-headless>=4.10,<5" \
|
||||
"onnxruntime>=1.20" \
|
||||
numpy scipy scikit-learn pillow matplotlib typing_extensions
|
||||
"$PY" -m pip install --no-deps oemer
|
||||
fi
|
||||
|
||||
SITE="$("$PY" -c "import oemer, pathlib; print(pathlib.Path(oemer.__file__).parent)")"
|
||||
mkdir -p "$SITE/checkpoints/unet_big" "$SITE/checkpoints/seg_net"
|
||||
if [[ ! -f "$SITE/checkpoints/unet_big/model.onnx" ]]; then
|
||||
curl -fL --retry 3 -o "$SITE/checkpoints/unet_big/model.onnx" \
|
||||
"https://github.com/BreezeWhite/oemer/releases/download/checkpoints/1st_model.onnx"
|
||||
fi
|
||||
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
|
||||
MPLBACKEND=Agg "$PY" -c "import oemer.ete, cv2; from cv2.typing import RotatedRect; print('oemer', cv2.__version__)"
|
||||
|
||||
echo "== Trim the Python prefix =="
|
||||
rm -rf \
|
||||
"$ROOT/build/python/lib/python3.12/test" \
|
||||
"$ROOT/build/python/lib/python3.12/ensurepip" \
|
||||
"$ROOT/build/python/lib/python3.12/idlelib" \
|
||||
"$ROOT/build/python/lib/python3.12/turtledemo" \
|
||||
"$ROOT/build/python/lib/python3.12/site-packages/pip" \
|
||||
"$ROOT/build/python/lib/python3.12/site-packages/pip-"*.dist-info \
|
||||
"$ROOT/build/python/lib/tcl9.0" \
|
||||
"$ROOT/build/python/lib/tcl9" \
|
||||
"$ROOT/build/python/lib/tk9.0" \
|
||||
"$ROOT/build/python/lib/itcl4.3.8" \
|
||||
"$ROOT/build/python/lib/libtcl9tk9.0.so" \
|
||||
"$ROOT/build/python/lib/libtcl9.0.so" \
|
||||
"$SITE/checkpoints/unet_big/weights.h5" \
|
||||
"$SITE/checkpoints/seg_net/weights.h5" || true
|
||||
find "$ROOT/build/python" -type d -name __pycache__ -prune -exec rm -rf {} + || true
|
||||
find "$ROOT/build/python" -type d -name tests -path '*/site-packages/*' -prune -exec rm -rf {} + || true
|
||||
find "$ROOT/build/python" -name '*.a' -delete || true
|
||||
# Leave the CPython binary and libpython intact. Stripping those breaks the interpreter.
|
||||
find "$ROOT/build/python/lib/python3.12/site-packages" -type f -name '*.so' \
|
||||
-exec strip --strip-unneeded {} + || true
|
||||
|
||||
if [[ ! -f "$ROOT/electron/icon.png" ]]; then
|
||||
"$PY" - <<'PY'
|
||||
from PIL import Image, ImageDraw
|
||||
image = Image.new("RGB", (512, 512), "#f3ecdf")
|
||||
draw = ImageDraw.Draw(image)
|
||||
draw.rounded_rectangle((206, 36, 306, 470), radius=48, fill="#e4cfae", outline="#6a4328", width=8)
|
||||
draw.ellipse((156, 92, 214, 150), outline="#6a4328", width=6)
|
||||
for y in (160, 210, 260, 330, 390, 450):
|
||||
draw.ellipse((228, y, 284, y + 48), fill="#1c3830")
|
||||
image.save("electron/icon.png")
|
||||
PY
|
||||
fi
|
||||
|
||||
echo "== Build the Next.js server =="
|
||||
npm run build
|
||||
rm -rf .next/standalone/public .next/standalone/.next/static .next/standalone/scripts
|
||||
mkdir -p .next/standalone/.next
|
||||
cp -a public .next/standalone/public
|
||||
cp -a .next/static .next/standalone/.next/static
|
||||
cp -a scripts .next/standalone/scripts
|
||||
|
||||
echo "== Package the AppImage =="
|
||||
export CSC_IDENTITY_AUTO_DISCOVERY=false
|
||||
npx electron-builder --linux AppImage
|
||||
|
||||
APPIMAGE="$(find dist -maxdepth 1 -name '*.AppImage' -printf '%T@ %p\n' | sort -nr | head -1 | cut -d' ' -f2-)"
|
||||
chmod +x "$APPIMAGE"
|
||||
SIZE="$(stat -c%s "$APPIMAGE")"
|
||||
echo "Built $APPIMAGE ($SIZE bytes)"
|
||||
LIMIT=$((600 * 1024 * 1024))
|
||||
if (( SIZE > LIMIT )); then
|
||||
echo "AppImage is over 600MB." >&2
|
||||
exit 1
|
||||
fi
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read one sheet-music image with oemer and print the MusicXML path.
|
||||
|
||||
oemer asks ONNX Runtime for CUDA first. This wrapper forces the CPU provider
|
||||
so the AppImage does not need a GPU.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
|
||||
def force_cpu() -> None:
|
||||
import onnxruntime as rt
|
||||
|
||||
original = rt.InferenceSession
|
||||
|
||||
def cpu_session(model, providers=None, **kwargs): # type: ignore[no-untyped-def]
|
||||
kwargs.pop("providers", None)
|
||||
return original(model, providers=["CPUExecutionProvider"], **kwargs)
|
||||
|
||||
rt.InferenceSession = cpu_session # type: ignore[method-assign]
|
||||
|
||||
|
||||
def prepare_png(src: str, dest: str) -> None:
|
||||
from PIL import Image, ImageOps
|
||||
|
||||
with Image.open(src) as image:
|
||||
rgb = ImageOps.exif_transpose(image).convert("RGB")
|
||||
if min(rgb.size) < 80:
|
||||
raise ValueError("This image is too small to read as sheet music.")
|
||||
rgb.thumbnail((2200, 2200))
|
||||
rgb.save(dest, "PNG")
|
||||
|
||||
|
||||
def recognize(png: str, out_dir: str) -> str:
|
||||
from argparse import Namespace
|
||||
|
||||
from oemer.ete import clear_data, extract
|
||||
|
||||
def once(deskew: bool) -> str:
|
||||
clear_data()
|
||||
args = Namespace(
|
||||
img_path=png,
|
||||
output_path=out_dir,
|
||||
use_tf=False,
|
||||
save_cache=False,
|
||||
without_deskew=not deskew,
|
||||
)
|
||||
return extract(args)
|
||||
|
||||
try:
|
||||
return once(True)
|
||||
except (Exception, SystemExit):
|
||||
traceback.print_exc()
|
||||
return once(False)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 3:
|
||||
print("usage: run_oemer.py IMAGE OUT_DIR", file=sys.stderr)
|
||||
return 2
|
||||
src, out_dir = sys.argv[1], sys.argv[2]
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
png = os.path.join(out_dir, "page.png")
|
||||
try:
|
||||
prepare_png(src, png)
|
||||
force_cpu()
|
||||
musicxml = recognize(png, out_dir)
|
||||
except Exception as exc: # noqa: BLE001 — surface a single readable failure
|
||||
print(f"{exc.__class__.__name__}: {exc}", file=sys.stderr)
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
if not musicxml or not os.path.isfile(musicxml):
|
||||
print("oemer did not write MusicXML", file=sys.stderr)
|
||||
return 1
|
||||
print(f"MUSICXML={musicxml}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user