Upload app.py with huggingface_hub
Browse files
app.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
| 1 |
import os
|
| 2 |
import base64
|
| 3 |
import inspect
|
| 4 |
-
import tempfile
|
| 5 |
from queue import Queue
|
| 6 |
import threading
|
| 7 |
import torch
|
| 8 |
-
from PIL import Image
|
| 9 |
from transformers import AutoProcessor, MiniCPMV4_6ForConditionalGeneration
|
| 10 |
import gradio as gr
|
| 11 |
|
|
@@ -22,7 +21,6 @@ GPT_FALLBACK_MODEL_ID = "gpt-5.4-mini"
|
|
| 22 |
GPT_MAX_COMPLETION_TOKENS = 4096
|
| 23 |
GPT_REASONING_EFFORT = "none"
|
| 24 |
NOTES_PROMPT = "Transcribe the musical notes in this image. Return only the transcription."
|
| 25 |
-
MAX_PREPROCESSED_SIDE = 1800
|
| 26 |
|
| 27 |
CAMERA_CAPTURE_JS = """
|
| 28 |
function () {
|
|
@@ -100,86 +98,6 @@ def supports_keyword(callable_obj, keyword):
|
|
| 100 |
return keyword in signature.parameters
|
| 101 |
|
| 102 |
|
| 103 |
-
def _ensure_horizontal_music(image: Image.Image) -> Image.Image:
|
| 104 |
-
width, height = image.size
|
| 105 |
-
if height > width:
|
| 106 |
-
return image.rotate(90, expand=True)
|
| 107 |
-
return image
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
def _score_training_orientation(image: Image.Image):
|
| 111 |
-
try:
|
| 112 |
-
import cv2
|
| 113 |
-
import numpy as np
|
| 114 |
-
except ImportError:
|
| 115 |
-
width, height = image.size
|
| 116 |
-
return width / max(height, 1)
|
| 117 |
-
|
| 118 |
-
gray = np.array(image.convert("L"))
|
| 119 |
-
height, width = gray.shape[:2]
|
| 120 |
-
if width == 0 or height == 0:
|
| 121 |
-
return -1
|
| 122 |
-
|
| 123 |
-
# Normalize out uneven camera lighting before comparing densities.
|
| 124 |
-
blur_size = min(max(min(width, height) // 6, 15) | 1, 201)
|
| 125 |
-
background = cv2.medianBlur(gray, blur_size)
|
| 126 |
-
normalized = cv2.divide(gray, background, scale=255)
|
| 127 |
-
ink = normalized < 128
|
| 128 |
-
|
| 129 |
-
band = max(width // 5, 1)
|
| 130 |
-
left_density = float(ink[:, :band].mean())
|
| 131 |
-
right_density = float(ink[:, -band:].mean())
|
| 132 |
-
# Treble clef + key sig always sit on the left, making that band denser.
|
| 133 |
-
return (width / max(height, 1)) * 2.0 + (left_density - right_density) * 15.0
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
def _normalize_to_training_orientation(image: Image.Image):
|
| 137 |
-
candidates = []
|
| 138 |
-
for rotation in (0, 90, 180, 270):
|
| 139 |
-
candidate = image.rotate(rotation, expand=True) if rotation else image.copy()
|
| 140 |
-
candidate = _ensure_horizontal_music(candidate)
|
| 141 |
-
score = _score_training_orientation(candidate)
|
| 142 |
-
candidates.append((score, rotation, candidate))
|
| 143 |
-
|
| 144 |
-
score, rotation, candidate = max(candidates, key=lambda item: item[0])
|
| 145 |
-
return candidate, rotation, score
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
def _limit_image_size(image: Image.Image) -> Image.Image:
|
| 149 |
-
width, height = image.size
|
| 150 |
-
max_side = max(width, height)
|
| 151 |
-
if max_side <= MAX_PREPROCESSED_SIDE:
|
| 152 |
-
return image
|
| 153 |
-
|
| 154 |
-
scale = MAX_PREPROCESSED_SIDE / max_side
|
| 155 |
-
return image.resize(
|
| 156 |
-
(int(width * scale), int(height * scale)),
|
| 157 |
-
Image.Resampling.LANCZOS,
|
| 158 |
-
)
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
def preprocess_sheet_music_image(image_path):
|
| 162 |
-
original = ImageOps.exif_transpose(Image.open(image_path)).convert("RGB")
|
| 163 |
-
original_size = original.size
|
| 164 |
-
image, rotation, orientation_score = _normalize_to_training_orientation(original)
|
| 165 |
-
image = _limit_image_size(image)
|
| 166 |
-
processed_size = image.size
|
| 167 |
-
|
| 168 |
-
temp_file = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
|
| 169 |
-
temp_path = temp_file.name
|
| 170 |
-
temp_file.close()
|
| 171 |
-
image.save(temp_path, format="PNG", optimize=True)
|
| 172 |
-
changed = processed_size != original_size
|
| 173 |
-
report = (
|
| 174 |
-
f"Preprocessing complete. Original: {original_size[0]}x{original_size[1]}; "
|
| 175 |
-
f"sent to models: {processed_size[0]}x{processed_size[1]}; "
|
| 176 |
-
f"selected rotation: {rotation} degrees; "
|
| 177 |
-
f"orientation score: {orientation_score:.2f}; "
|
| 178 |
-
f"changed: {'yes' if changed else 'no'}. "
|
| 179 |
-
"Treble clef/key-signature content is expected at the top-left of the preview."
|
| 180 |
-
)
|
| 181 |
-
return image, temp_path, report
|
| 182 |
-
|
| 183 |
print("Loading processor...")
|
| 184 |
processor = AutoProcessor.from_pretrained(ORIGINAL_MODEL_ID, trust_remote_code=True)
|
| 185 |
|
|
@@ -343,26 +261,20 @@ def warmup_models():
|
|
| 343 |
return
|
| 344 |
|
| 345 |
print("Warming up local models...")
|
| 346 |
-
image
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
print(f" Warming {name} model...")
|
| 356 |
-
try:
|
| 357 |
-
generate_model_text(model, image, max_new_tokens=8)
|
| 358 |
-
except Exception as e:
|
| 359 |
-
print(f" Warmup failed for {name} model: {type(e).__name__}: {e}")
|
| 360 |
-
print("Model warmup complete.")
|
| 361 |
-
finally:
|
| 362 |
try:
|
| 363 |
-
|
| 364 |
-
except
|
| 365 |
-
|
|
|
|
| 366 |
|
| 367 |
|
| 368 |
warmup_models()
|
|
@@ -483,18 +395,18 @@ def _run_stream(index, stream, updates):
|
|
| 483 |
def predict_all(image_path):
|
| 484 |
if image_path is None:
|
| 485 |
message = "Please upload an image."
|
| 486 |
-
yield
|
| 487 |
return
|
| 488 |
|
| 489 |
-
image
|
| 490 |
updates = Queue()
|
| 491 |
outputs = ["", "", ""]
|
| 492 |
-
yield
|
| 493 |
|
| 494 |
streams = [
|
| 495 |
stream_model_text(finetuned_model, image.copy(), "fine-tuned"),
|
| 496 |
stream_model_text(original_model, image.copy(), "original"),
|
| 497 |
-
stream_gpt_text(
|
| 498 |
]
|
| 499 |
|
| 500 |
threads = [
|
|
@@ -517,16 +429,10 @@ def predict_all(image_path):
|
|
| 517 |
continue
|
| 518 |
|
| 519 |
outputs[index] = text
|
| 520 |
-
yield (
|
| 521 |
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
thread.join()
|
| 525 |
-
finally:
|
| 526 |
-
try:
|
| 527 |
-
os.unlink(processed_image_path)
|
| 528 |
-
except OSError:
|
| 529 |
-
pass
|
| 530 |
|
| 531 |
|
| 532 |
if HAS_SPACES:
|
|
@@ -550,30 +456,17 @@ with gr.Blocks(**blocks_kwargs) as demo:
|
|
| 550 |
"""
|
| 551 |
)
|
| 552 |
|
| 553 |
-
gr.Markdown(
|
| 554 |
-
"Camera: allow camera access, use the rear camera, frame the page normally with the top-left of the music at the top-left of the preview, then tap or click the preview to capture."
|
| 555 |
-
)
|
| 556 |
image_input = gr.Image(
|
| 557 |
type="filepath",
|
| 558 |
-
label="Sheet Music Image
|
| 559 |
sources=["webcam", "upload", "clipboard"],
|
| 560 |
webcam_options=gr.WebcamOptions(
|
| 561 |
mirror=False,
|
| 562 |
-
constraints={"facingMode":
|
| 563 |
),
|
| 564 |
-
placeholder="Use
|
| 565 |
elem_id="sheet-music-input",
|
| 566 |
)
|
| 567 |
-
processed_image_output = gr.Image(
|
| 568 |
-
type="pil",
|
| 569 |
-
label="Preprocessed Image Sent to All Models",
|
| 570 |
-
interactive=False,
|
| 571 |
-
)
|
| 572 |
-
preprocessing_report_output = gr.Textbox(
|
| 573 |
-
label="Preprocessing Confirmation",
|
| 574 |
-
interactive=False,
|
| 575 |
-
lines=2,
|
| 576 |
-
)
|
| 577 |
gr.Examples(
|
| 578 |
examples=[
|
| 579 |
["examples/000100005-1_1_1.png"],
|
|
@@ -601,13 +494,7 @@ with gr.Blocks(**blocks_kwargs) as demo:
|
|
| 601 |
notes_btn.click(
|
| 602 |
fn=predict_all,
|
| 603 |
inputs=[image_input],
|
| 604 |
-
outputs=[
|
| 605 |
-
processed_image_output,
|
| 606 |
-
preprocessing_report_output,
|
| 607 |
-
finetuned_output,
|
| 608 |
-
original_output,
|
| 609 |
-
gpt_output,
|
| 610 |
-
],
|
| 611 |
)
|
| 612 |
|
| 613 |
launch_kwargs = {
|
|
|
|
| 1 |
import os
|
| 2 |
import base64
|
| 3 |
import inspect
|
|
|
|
| 4 |
from queue import Queue
|
| 5 |
import threading
|
| 6 |
import torch
|
| 7 |
+
from PIL import Image
|
| 8 |
from transformers import AutoProcessor, MiniCPMV4_6ForConditionalGeneration
|
| 9 |
import gradio as gr
|
| 10 |
|
|
|
|
| 21 |
GPT_MAX_COMPLETION_TOKENS = 4096
|
| 22 |
GPT_REASONING_EFFORT = "none"
|
| 23 |
NOTES_PROMPT = "Transcribe the musical notes in this image. Return only the transcription."
|
|
|
|
| 24 |
|
| 25 |
CAMERA_CAPTURE_JS = """
|
| 26 |
function () {
|
|
|
|
| 98 |
return keyword in signature.parameters
|
| 99 |
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
print("Loading processor...")
|
| 102 |
processor = AutoProcessor.from_pretrained(ORIGINAL_MODEL_ID, trust_remote_code=True)
|
| 103 |
|
|
|
|
| 261 |
return
|
| 262 |
|
| 263 |
print("Warming up local models...")
|
| 264 |
+
image = Image.open(warmup_path).convert("RGB")
|
| 265 |
+
for name, model in (
|
| 266 |
+
("fine-tuned", finetuned_model),
|
| 267 |
+
("original", original_model),
|
| 268 |
+
):
|
| 269 |
+
if model is None:
|
| 270 |
+
print(f" Skipping {name} warmup; model failed to load.")
|
| 271 |
+
continue
|
| 272 |
+
print(f" Warming {name} model...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
try:
|
| 274 |
+
generate_model_text(model, image, max_new_tokens=8)
|
| 275 |
+
except Exception as e:
|
| 276 |
+
print(f" Warmup failed for {name} model: {type(e).__name__}: {e}")
|
| 277 |
+
print("Model warmup complete.")
|
| 278 |
|
| 279 |
|
| 280 |
warmup_models()
|
|
|
|
| 395 |
def predict_all(image_path):
|
| 396 |
if image_path is None:
|
| 397 |
message = "Please upload an image."
|
| 398 |
+
yield message, message, message
|
| 399 |
return
|
| 400 |
|
| 401 |
+
image = Image.open(image_path).convert("RGB")
|
| 402 |
updates = Queue()
|
| 403 |
outputs = ["", "", ""]
|
| 404 |
+
yield outputs[0], outputs[1], outputs[2]
|
| 405 |
|
| 406 |
streams = [
|
| 407 |
stream_model_text(finetuned_model, image.copy(), "fine-tuned"),
|
| 408 |
stream_model_text(original_model, image.copy(), "original"),
|
| 409 |
+
stream_gpt_text(image_path),
|
| 410 |
]
|
| 411 |
|
| 412 |
threads = [
|
|
|
|
| 429 |
continue
|
| 430 |
|
| 431 |
outputs[index] = text
|
| 432 |
+
yield (*outputs,)
|
| 433 |
|
| 434 |
+
for thread in threads:
|
| 435 |
+
thread.join()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
|
| 437 |
|
| 438 |
if HAS_SPACES:
|
|
|
|
| 456 |
"""
|
| 457 |
)
|
| 458 |
|
|
|
|
|
|
|
|
|
|
| 459 |
image_input = gr.Image(
|
| 460 |
type="filepath",
|
| 461 |
+
label="Sheet Music Image",
|
| 462 |
sources=["webcam", "upload", "clipboard"],
|
| 463 |
webcam_options=gr.WebcamOptions(
|
| 464 |
mirror=False,
|
| 465 |
+
constraints={"facingMode": "environment"},
|
| 466 |
),
|
| 467 |
+
placeholder="Use the camera to capture the sheet music, then click Transcribe Music.",
|
| 468 |
elem_id="sheet-music-input",
|
| 469 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
gr.Examples(
|
| 471 |
examples=[
|
| 472 |
["examples/000100005-1_1_1.png"],
|
|
|
|
| 494 |
notes_btn.click(
|
| 495 |
fn=predict_all,
|
| 496 |
inputs=[image_input],
|
| 497 |
+
outputs=[finetuned_output, original_output, gpt_output],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
)
|
| 499 |
|
| 500 |
launch_kwargs = {
|