Update app.py
Browse files
app.py
CHANGED
|
@@ -8,25 +8,27 @@ from werkzeug.utils import secure_filename
|
|
| 8 |
from langchain_groq import ChatGroq
|
| 9 |
from langgraph.prebuilt import create_react_agent
|
| 10 |
from pdf2image import convert_from_path, convert_from_bytes
|
| 11 |
-
from typing import Dict, TypedDict, Optional, Any
|
|
|
|
| 12 |
from langgraph.graph import StateGraph, END
|
| 13 |
import uuid
|
| 14 |
import shutil, time, functools
|
| 15 |
from io import BytesIO
|
| 16 |
from pathlib import Path
|
| 17 |
-
import os
|
| 18 |
from utils.block_relation_builder import block_builder, separate_scripts, transform_logic_to_action_flow, analyze_opcode_counts
|
| 19 |
from difflib import get_close_matches
|
| 20 |
import torch
|
| 21 |
from transformers import AutoImageProcessor, AutoModel
|
| 22 |
-
from pathlib import Path
|
| 23 |
-
from io import BytesIO
|
| 24 |
import torch
|
| 25 |
import json
|
| 26 |
import cv2
|
| 27 |
-
# hashing & image-match
|
| 28 |
from imagededup.methods import PHash
|
| 29 |
from image_match.goldberg import ImageSignature
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
# DINOv2 model id
|
| 31 |
DINOV2_MODEL = "facebook/dinov2-small"
|
| 32 |
|
|
@@ -346,13 +348,225 @@ def cosine_similarity(a, b):
|
|
| 346 |
return float(np.dot(a, b))
|
| 347 |
|
| 348 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
# --------------------------
|
| 350 |
# Choose best candidate helper
|
| 351 |
# --------------------------
|
| 352 |
from collections import defaultdict
|
| 353 |
import math
|
| 354 |
|
| 355 |
-
def choose_top_candidates(embedding_results, phash_results, imgmatch_results, top_k=10,
|
|
|
|
| 356 |
"""
|
| 357 |
embedding_results: list of (path, emb_sim) where emb_sim roughly in [-1,1] (we'll clamp to 0..1)
|
| 358 |
phash_results: list of (path, hamming, ph_sim) where ph_sim in [0,1]
|
|
@@ -1383,60 +1597,99 @@ def processed_page_node(state: GameState):
|
|
| 1383 |
state["processing"]= False
|
| 1384 |
return state
|
| 1385 |
|
| 1386 |
-
def extract_images_from_pdf(pdf_stream: io.BytesIO):
|
| 1387 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1388 |
manipulated_json = {}
|
| 1389 |
-
img_elements = []
|
| 1390 |
try:
|
| 1391 |
-
|
| 1392 |
-
|
| 1393 |
-
|
| 1394 |
-
|
| 1395 |
-
|
| 1396 |
-
|
| 1397 |
-
|
| 1398 |
-
|
| 1399 |
-
|
| 1400 |
-
file=pdf_stream,
|
| 1401 |
-
strategy="hi_res",
|
| 1402 |
-
# strategy="fast",
|
| 1403 |
-
extract_image_block_types=["Image"],
|
| 1404 |
-
hi_res_model_name="yolox",
|
| 1405 |
-
extract_image_block_to_payload=True,
|
| 1406 |
-
)
|
| 1407 |
-
print(f"ELEMENTS")
|
| 1408 |
-
except Exception as e:
|
| 1409 |
-
raise RuntimeError(
|
| 1410 |
-
f"β Failed to extract images from PDF: {str(e)}")
|
| 1411 |
-
|
| 1412 |
file_elements = [element.to_dict() for element in elements]
|
| 1413 |
-
print(f"========== file elements: \n{file_elements}")
|
| 1414 |
-
|
| 1415 |
sprite_count = 1
|
| 1416 |
for el in file_elements:
|
| 1417 |
-
|
| 1418 |
-
|
|
|
|
|
|
|
| 1419 |
continue
|
| 1420 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1421 |
manipulated_json[f"Sprite {sprite_count}"] = {
|
| 1422 |
-
"base64":
|
| 1423 |
-
"file-path":
|
|
|
|
|
|
|
| 1424 |
}
|
|
|
|
| 1425 |
sprite_count += 1
|
|
|
|
| 1426 |
return manipulated_json
|
| 1427 |
except Exception as e:
|
| 1428 |
raise RuntimeError(f"β Error in extract_images_from_pdf: {str(e)}")
|
| 1429 |
-
|
| 1430 |
-
|
|
|
|
| 1431 |
def similarity_matching(sprites_data: dict, project_folder: str, top_k: int = 1, min_similarity: float = None) -> str:
|
| 1432 |
print("π Running similarity matchingβ¦")
|
| 1433 |
-
import os
|
| 1434 |
-
import json
|
| 1435 |
os.makedirs(project_folder, exist_ok=True)
|
| 1436 |
|
| 1437 |
-
backdrop_base_path =
|
| 1438 |
-
sprite_base_path =
|
| 1439 |
-
code_blocks_path =
|
|
|
|
|
|
|
| 1440 |
|
| 1441 |
project_json_path = os.path.join(project_folder, "project.json")
|
| 1442 |
|
|
@@ -1449,73 +1702,64 @@ def similarity_matching(sprites_data: dict, project_folder: str, top_k: int = 1,
|
|
| 1449 |
sprite_base64.append(sprite["base64"])
|
| 1450 |
|
| 1451 |
sprite_images_bytes = []
|
|
|
|
| 1452 |
for b64 in sprite_base64:
|
| 1453 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1454 |
buffer = BytesIO()
|
| 1455 |
img.save(buffer, format="PNG")
|
| 1456 |
buffer.seek(0)
|
| 1457 |
sprite_images_bytes.append(buffer)
|
| 1458 |
-
|
| 1459 |
-
|
| 1460 |
-
|
| 1461 |
-
|
| 1462 |
-
def hybrid_similarity_matching(sprite_images_bytes, sprite_ids,
|
| 1463 |
-
min_similarity=None, top_k=5, method_weights=(0.5, 0.3, 0.2)):
|
| 1464 |
-
"""
|
| 1465 |
-
Hybrid similarity matching using DINOv2 embeddings, perceptual hashing, and image signatures
|
| 1466 |
-
|
| 1467 |
-
Args:
|
| 1468 |
-
sprite_images_bytes: List of image bytes
|
| 1469 |
-
sprite_ids: List of sprite identifiers
|
| 1470 |
-
blocks_dir: Directory containing reference blocks
|
| 1471 |
-
min_similarity: Minimum similarity threshold
|
| 1472 |
-
top_k: Number of top matches to return
|
| 1473 |
-
method_weights: Weights for (embedding, phash, image_signature) methods
|
| 1474 |
-
|
| 1475 |
-
Returns:
|
| 1476 |
-
per_sprite_matched_indices, per_sprite_scores, paths_list
|
| 1477 |
-
"""
|
| 1478 |
-
import imagehash as phash
|
| 1479 |
-
from image_match.goldberg import ImageSignature
|
| 1480 |
-
import math
|
| 1481 |
-
from collections import defaultdict
|
| 1482 |
-
|
| 1483 |
-
# Load reference data
|
| 1484 |
embeddings_path = os.path.join(BLOCKS_DIR, "hybrid_embeddings.json")
|
| 1485 |
-
hash_path = os.path.join(BLOCKS_DIR, "phash_data.json")
|
| 1486 |
signature_path = os.path.join(BLOCKS_DIR, "signature_data.json")
|
| 1487 |
-
|
| 1488 |
# Load embeddings
|
| 1489 |
-
|
| 1490 |
-
|
| 1491 |
-
|
| 1492 |
-
|
|
|
|
|
|
|
| 1493 |
hash_dict = {}
|
| 1494 |
if os.path.exists(hash_path):
|
| 1495 |
-
|
| 1496 |
-
|
| 1497 |
-
|
| 1498 |
-
|
| 1499 |
-
|
| 1500 |
-
|
| 1501 |
-
|
| 1502 |
-
|
| 1503 |
-
|
|
|
|
|
|
|
|
|
|
| 1504 |
signature_dict = {}
|
| 1505 |
-
|
| 1506 |
if os.path.exists(signature_path):
|
| 1507 |
-
|
| 1508 |
-
|
| 1509 |
-
|
| 1510 |
-
|
| 1511 |
-
|
| 1512 |
-
|
| 1513 |
-
|
| 1514 |
-
|
| 1515 |
-
|
|
|
|
|
|
|
|
|
|
| 1516 |
paths_list = []
|
| 1517 |
embeddings_list = []
|
| 1518 |
-
|
| 1519 |
if isinstance(embedding_json, dict):
|
| 1520 |
for p, emb in embedding_json.items():
|
| 1521 |
if isinstance(emb, dict):
|
|
@@ -1539,294 +1783,77 @@ def similarity_matching(sprites_data: dict, project_folder: str, top_k: int = 1,
|
|
| 1539 |
continue
|
| 1540 |
paths_list.append(os.path.normpath(str(p)))
|
| 1541 |
embeddings_list.append(np.asarray(emb, dtype=np.float32))
|
| 1542 |
-
|
| 1543 |
if len(paths_list) == 0:
|
| 1544 |
-
|
| 1545 |
-
|
|
|
|
|
|
|
| 1546 |
ref_matrix = np.vstack(embeddings_list).astype(np.float32)
|
| 1547 |
|
| 1548 |
-
#
|
| 1549 |
-
|
| 1550 |
-
|
| 1551 |
-
|
| 1552 |
-
|
| 1553 |
-
|
| 1554 |
-
|
| 1555 |
-
|
| 1556 |
-
# Convert bytes to PIL for processing
|
| 1557 |
sprite_pil = Image.open(sprite_bytes)
|
| 1558 |
-
|
| 1559 |
-
|
| 1560 |
-
|
| 1561 |
-
continue
|
| 1562 |
-
|
| 1563 |
-
# Enhance image
|
| 1564 |
-
enhanced_sprite = process_image_cv2_from_pil(sprite_pil, scale=2)
|
| 1565 |
-
if enhanced_sprite is None:
|
| 1566 |
-
enhanced_sprite = sprite_pil
|
| 1567 |
-
|
| 1568 |
-
# 1. Compute DINOv2 embedding
|
| 1569 |
sprite_emb = get_dinov2_embedding_from_pil(preprocess_for_model(enhanced_sprite))
|
| 1570 |
-
if sprite_emb is None
|
| 1571 |
-
|
| 1572 |
-
|
| 1573 |
-
# 2. Compute perceptual hash
|
| 1574 |
sprite_hash_arr = preprocess_for_hash(enhanced_sprite)
|
| 1575 |
sprite_phash = None
|
| 1576 |
if sprite_hash_arr is not None:
|
| 1577 |
-
try:
|
| 1578 |
-
|
| 1579 |
-
|
| 1580 |
-
|
| 1581 |
-
|
| 1582 |
-
# 3. Compute image signature
|
| 1583 |
sprite_sig = None
|
| 1584 |
-
|
| 1585 |
-
|
| 1586 |
-
|
| 1587 |
-
|
| 1588 |
-
|
| 1589 |
-
|
| 1590 |
-
|
| 1591 |
-
|
| 1592 |
-
|
| 1593 |
-
|
| 1594 |
-
|
| 1595 |
-
|
| 1596 |
-
|
| 1597 |
-
|
| 1598 |
-
|
| 1599 |
-
|
| 1600 |
-
|
| 1601 |
-
|
| 1602 |
-
|
| 1603 |
-
|
| 1604 |
-
|
| 1605 |
-
|
| 1606 |
-
|
| 1607 |
-
# Phash similarity
|
| 1608 |
-
ph_sim = 0.0
|
| 1609 |
-
if sprite_phash is not None and ref_path in hash_dict:
|
| 1610 |
-
try:
|
| 1611 |
-
ref_hash = hash_dict[ref_path]
|
| 1612 |
-
hd = phash.hamming_distance(sprite_phash, ref_hash)
|
| 1613 |
-
ph_sim = max(0.0, 1.0 - (hd / 64.0)) # Normalize to [0,1]
|
| 1614 |
-
except:
|
| 1615 |
-
pass
|
| 1616 |
-
phash_results.append((ref_path, ph_sim))
|
| 1617 |
-
|
| 1618 |
-
# Signature similarity
|
| 1619 |
-
sig_sim = 0.0
|
| 1620 |
-
if sprite_sig is not None and ref_path in signature_dict:
|
| 1621 |
-
try:
|
| 1622 |
-
ref_sig = signature_dict[ref_path]
|
| 1623 |
-
dist = gis.normalized_distance(ref_sig, sprite_sig)
|
| 1624 |
-
sig_sim = max(0.0, 1.0 - dist)
|
| 1625 |
-
except:
|
| 1626 |
-
pass
|
| 1627 |
-
signature_results.append((ref_path, sig_sim))
|
| 1628 |
-
|
| 1629 |
-
# Combine similarities using weighted approach
|
| 1630 |
-
def normalize_scores(scores):
|
| 1631 |
-
"""Normalize scores to [0,1] range"""
|
| 1632 |
-
if not scores:
|
| 1633 |
-
return {}
|
| 1634 |
-
vals = [s for _, s in scores if not math.isnan(s)]
|
| 1635 |
-
if not vals:
|
| 1636 |
-
return {p: 0.0 for p, _ in scores}
|
| 1637 |
-
vmin, vmax = min(vals), max(vals)
|
| 1638 |
-
if vmax == vmin:
|
| 1639 |
-
return {p: 1.0 if s == vmax else 0.0 for p, s in scores}
|
| 1640 |
-
return {p: (s - vmin) / (vmax - vmin) for p, s in scores}
|
| 1641 |
-
|
| 1642 |
-
# Normalize each method's scores
|
| 1643 |
-
emb_norm = normalize_scores(embedding_results)
|
| 1644 |
-
ph_norm = normalize_scores(phash_results)
|
| 1645 |
-
sig_norm = normalize_scores(signature_results)
|
| 1646 |
-
|
| 1647 |
-
# Calculate weighted combined scores
|
| 1648 |
-
w_emb, w_ph, w_sig = method_weights
|
| 1649 |
-
combined_scores = []
|
| 1650 |
-
|
| 1651 |
-
for ref_path in paths_list:
|
| 1652 |
-
combined_score = (w_emb * emb_norm.get(ref_path, 0.0) +
|
| 1653 |
-
w_ph * ph_norm.get(ref_path, 0.0) +
|
| 1654 |
-
w_sig * sig_norm.get(ref_path, 0.0))
|
| 1655 |
-
combined_scores.append((ref_path, combined_score))
|
| 1656 |
-
|
| 1657 |
-
# Sort by combined score and apply thresholds
|
| 1658 |
-
combined_scores.sort(key=lambda x: x[1], reverse=True)
|
| 1659 |
-
|
| 1660 |
-
# Filter by minimum similarity if specified
|
| 1661 |
-
if min_similarity is not None:
|
| 1662 |
-
combined_scores = [(p, s) for p, s in combined_scores if s >= float(min_similarity)]
|
| 1663 |
-
|
| 1664 |
-
# Get top-k matches
|
| 1665 |
-
top_matches = combined_scores[:int(top_k)]
|
| 1666 |
-
|
| 1667 |
-
# Convert to indices and scores
|
| 1668 |
-
matched_indices = []
|
| 1669 |
-
matched_scores = []
|
| 1670 |
-
|
| 1671 |
-
for ref_path, score in top_matches:
|
| 1672 |
-
try:
|
| 1673 |
-
idx = paths_list.index(ref_path)
|
| 1674 |
-
matched_indices.append(idx)
|
| 1675 |
-
matched_scores.append(score)
|
| 1676 |
-
except ValueError:
|
| 1677 |
-
continue
|
| 1678 |
-
|
| 1679 |
-
per_sprite_matched_indices.append(matched_indices)
|
| 1680 |
-
per_sprite_scores.append(matched_scores)
|
| 1681 |
-
|
| 1682 |
-
print(f"Sprite '{sprite_id}' matched {len(matched_indices)} references with scores: {matched_scores}")
|
| 1683 |
-
|
| 1684 |
-
return per_sprite_matched_indices, per_sprite_scores, paths_list
|
| 1685 |
-
|
| 1686 |
-
def choose_top_candidates_advanced(embedding_results, phash_results, imgmatch_results, top_k=10,
|
| 1687 |
-
method_weights=(0.5, 0.3, 0.2), verbose=True):
|
| 1688 |
-
"""
|
| 1689 |
-
Advanced candidate selection using multiple ranking methods
|
| 1690 |
-
|
| 1691 |
-
Args:
|
| 1692 |
-
embedding_results: list of (path, emb_sim)
|
| 1693 |
-
phash_results: list of (path, hamming, ph_sim)
|
| 1694 |
-
imgmatch_results: list of (path, dist, im_sim)
|
| 1695 |
-
top_k: number of top candidates to return
|
| 1696 |
-
method_weights: weights for (emb, phash, imgmatch)
|
| 1697 |
-
verbose: whether to print detailed results
|
| 1698 |
-
|
| 1699 |
-
Returns:
|
| 1700 |
-
dict with top candidates from different methods and final selection
|
| 1701 |
-
"""
|
| 1702 |
-
import math
|
| 1703 |
-
from collections import defaultdict
|
| 1704 |
-
|
| 1705 |
-
# Build dicts for quick lookup
|
| 1706 |
-
emb_map = {p: float(s) for p, s in embedding_results}
|
| 1707 |
-
ph_map = {p: float(sim) for p, _, sim in phash_results}
|
| 1708 |
-
im_map = {p: float(sim) for p, _, sim in imgmatch_results}
|
| 1709 |
-
|
| 1710 |
-
# Universe of candidates (union)
|
| 1711 |
-
all_paths = sorted(set(list(emb_map.keys()) + list(ph_map.keys()) + list(im_map.keys())))
|
| 1712 |
-
|
| 1713 |
-
# Normalize each metric across candidates to [0,1]
|
| 1714 |
-
def normalize_map(m):
|
| 1715 |
-
vals = [m.get(p, None) for p in all_paths]
|
| 1716 |
-
present = [v for v in vals if v is not None and not math.isnan(v)]
|
| 1717 |
-
if not present:
|
| 1718 |
-
return {p: 0.0 for p in all_paths}
|
| 1719 |
-
vmin, vmax = min(present), max(present)
|
| 1720 |
-
if vmax == vmin:
|
| 1721 |
-
return {p: (1.0 if (m.get(p, None) is not None) else 0.0) for p in all_paths}
|
| 1722 |
-
norm = {}
|
| 1723 |
-
for p in all_paths:
|
| 1724 |
-
v = m.get(p, None)
|
| 1725 |
-
if v is None or math.isnan(v):
|
| 1726 |
-
norm[p] = 0.0
|
| 1727 |
-
else:
|
| 1728 |
-
norm[p] = max(0.0, min(1.0, (v - vmin) / (vmax - vmin)))
|
| 1729 |
-
return norm
|
| 1730 |
-
|
| 1731 |
-
# For embeddings, clamp negatives to 0 first
|
| 1732 |
-
emb_map_clamped = {p: max(0.0, v) for p, v in emb_map.items()}
|
| 1733 |
-
|
| 1734 |
-
emb_norm = normalize_map(emb_map_clamped)
|
| 1735 |
-
ph_norm = normalize_map(ph_map)
|
| 1736 |
-
im_norm = normalize_map(im_map)
|
| 1737 |
-
|
| 1738 |
-
# Method A: Normalized weighted average
|
| 1739 |
-
w_emb, w_ph, w_im = method_weights
|
| 1740 |
-
weighted_scores = {}
|
| 1741 |
-
for p in all_paths:
|
| 1742 |
-
weighted_scores[p] = (w_emb * emb_norm.get(p, 0.0)
|
| 1743 |
-
+ w_ph * ph_norm.get(p, 0.0)
|
| 1744 |
-
+ w_im * im_norm.get(p, 0.0))
|
| 1745 |
-
|
| 1746 |
-
top_weighted = sorted(weighted_scores.items(), key=lambda x: x[1], reverse=True)[:top_k]
|
| 1747 |
-
|
| 1748 |
-
# Method B: Rank-sum (Borda)
|
| 1749 |
-
def ranks_from_map(m_norm):
|
| 1750 |
-
items = sorted(m_norm.items(), key=lambda x: x[1], reverse=True)
|
| 1751 |
-
ranks = {}
|
| 1752 |
-
for i, (p, _) in enumerate(items):
|
| 1753 |
-
ranks[p] = i + 1 # 1-based
|
| 1754 |
-
worst = len(items) + 1
|
| 1755 |
-
for p in all_paths:
|
| 1756 |
-
if p not in ranks:
|
| 1757 |
-
ranks[p] = worst
|
| 1758 |
-
return ranks
|
| 1759 |
-
|
| 1760 |
-
rank_emb = ranks_from_map(emb_norm)
|
| 1761 |
-
rank_ph = ranks_from_map(ph_norm)
|
| 1762 |
-
rank_im = ranks_from_map(im_norm)
|
| 1763 |
-
|
| 1764 |
-
rank_sum = {}
|
| 1765 |
-
for p in all_paths:
|
| 1766 |
-
rank_sum[p] = rank_emb.get(p, 9999) + rank_ph.get(p, 9999) + rank_im.get(p, 9999)
|
| 1767 |
-
top_rank_sum = sorted(rank_sum.items(), key=lambda x: x[1])[:top_k] # smaller is better
|
| 1768 |
-
|
| 1769 |
-
# Method C: Harmonic mean
|
| 1770 |
-
harm_scores = {}
|
| 1771 |
-
for p in all_paths:
|
| 1772 |
-
a = emb_norm.get(p, 0.0)
|
| 1773 |
-
b = ph_norm.get(p, 0.0)
|
| 1774 |
-
c = im_norm.get(p, 0.0)
|
| 1775 |
-
if a + b + c == 0 or a == 0 or b == 0 or c == 0:
|
| 1776 |
-
harm = 0.0
|
| 1777 |
else:
|
| 1778 |
-
|
| 1779 |
-
|
| 1780 |
-
|
| 1781 |
-
|
| 1782 |
-
|
| 1783 |
-
|
| 1784 |
-
|
| 1785 |
-
|
| 1786 |
-
|
| 1787 |
-
|
| 1788 |
-
|
| 1789 |
-
|
| 1790 |
-
"im_norm": im_norm,
|
| 1791 |
-
"weighted_topk": top_weighted,
|
| 1792 |
-
"rank_sum_topk": top_rank_sum,
|
| 1793 |
-
"harmonic_topk": top_harm,
|
| 1794 |
-
"consensus_topk": list(cons_set),
|
| 1795 |
-
"weighted_scores_full": weighted_scores,
|
| 1796 |
-
"rank_sum_full": rank_sum,
|
| 1797 |
-
"harmonic_full": harm_scores
|
| 1798 |
-
}
|
| 1799 |
-
|
| 1800 |
-
if verbose:
|
| 1801 |
-
print(f"\nTop by Weighted Average (weights emb,ph,img = {w_emb:.2f},{w_ph:.2f},{w_im:.2f}):")
|
| 1802 |
-
for i,(p,s) in enumerate(result["weighted_topk"], start=1):
|
| 1803 |
-
print(f" {i}. {p} score={s:.4f} emb={emb_norm.get(p,0):.3f} ph={ph_norm.get(p,0):.3f} im={im_norm.get(p,0):.3f}")
|
| 1804 |
-
|
| 1805 |
-
print("\nTop by Rank-sum (lower is better):")
|
| 1806 |
-
for i,(p,s) in enumerate(result["rank_sum_topk"], start=1):
|
| 1807 |
-
print(f" {i}. {p} rank_sum={s} emb_rank={rank_emb.get(p)} ph_rank={rank_ph.get(p)} img_rank={rank_im.get(p)}")
|
| 1808 |
-
|
| 1809 |
-
print("\nTop by Harmonic mean:")
|
| 1810 |
-
for i,(p,s) in enumerate(result["harmonic_topk"], start=1):
|
| 1811 |
-
print(f" {i}. {p} harm={s:.4f} emb={emb_norm.get(p,0):.3f} ph={ph_norm.get(p,0):.3f} im={im_norm.get(p,0):.3f}")
|
| 1812 |
-
|
| 1813 |
-
print(f"\nConsensus (in top-{top_k} of ALL metrics): {result['consensus_topk']}")
|
| 1814 |
|
| 1815 |
-
|
| 1816 |
-
final = None
|
| 1817 |
-
if len(result["consensus_topk"]) > 0:
|
| 1818 |
-
# Choose best-weighted among consensus
|
| 1819 |
-
consensus = result["consensus_topk"]
|
| 1820 |
-
best = max(consensus, key=lambda p: result["weighted_scores_full"].get(p, 0.0))
|
| 1821 |
-
final = best
|
| 1822 |
-
else:
|
| 1823 |
-
final = result["weighted_topk"][0][0] if result["weighted_topk"] else None
|
| 1824 |
|
| 1825 |
-
result["final_selection"] = final
|
| 1826 |
-
return result
|
| 1827 |
-
|
| 1828 |
# Use hybrid matching system
|
| 1829 |
-
# BLOCKS_DIR = r"D:\DEV PATEL\2025\scratch_VLM\scratch_agent\blocks"
|
| 1830 |
per_sprite_matched_indices, per_sprite_scores, paths_list = hybrid_similarity_matching(
|
| 1831 |
sprite_images_bytes, sprite_ids, min_similarity, top_k, method_weights=(0.5, 0.3, 0.2)
|
| 1832 |
)
|
|
@@ -1839,78 +1866,47 @@ def similarity_matching(sprites_data: dict, project_folder: str, top_k: int = 1,
|
|
| 1839 |
copied_sprite_folders = set()
|
| 1840 |
copied_backdrop_folders = set()
|
| 1841 |
|
| 1842 |
-
# Flatten unique matched indices to process copying once per folder
|
| 1843 |
matched_indices = sorted({idx for lst in per_sprite_matched_indices for idx in lst})
|
| 1844 |
print("matched_indices------------------>",matched_indices)
|
| 1845 |
|
| 1846 |
-
import shutil
|
| 1847 |
-
import json
|
| 1848 |
-
import os
|
| 1849 |
-
from pathlib import Path
|
| 1850 |
-
|
| 1851 |
-
# normalize base paths once before the loop
|
| 1852 |
sprite_base_p = Path(sprite_base_path).resolve(strict=False)
|
| 1853 |
backdrop_base_p = Path(backdrop_base_path).resolve(strict=False)
|
| 1854 |
project_folder_p = Path(project_folder)
|
| 1855 |
project_folder_p.mkdir(parents=True, exist_ok=True)
|
| 1856 |
|
| 1857 |
-
copied_sprite_folders = set()
|
| 1858 |
-
copied_backdrop_folders = set()
|
| 1859 |
-
|
| 1860 |
def display_like_windows_no_lead(p: Path) -> str:
|
| 1861 |
-
|
| 1862 |
-
For human-readable logs only β convert Path to a string like:
|
| 1863 |
-
"app\\blocks\\Backdrops\\Castle 2.sb3" (no leading slash).
|
| 1864 |
-
"""
|
| 1865 |
-
s = p.as_posix() # forward-slash string, safe for Path objects
|
| 1866 |
if s.startswith("/"):
|
| 1867 |
s = s[1:]
|
| 1868 |
return s.replace("/", "\\")
|
| 1869 |
|
| 1870 |
def is_subpath(child: Path, parent: Path) -> bool:
|
| 1871 |
-
"""Robust membership test: is child under parent?"""
|
| 1872 |
try:
|
| 1873 |
-
# use non-strict resolve only if needed, but avoid exceptions
|
| 1874 |
child.relative_to(parent)
|
| 1875 |
return True
|
| 1876 |
except Exception:
|
| 1877 |
return False
|
| 1878 |
-
|
| 1879 |
-
#
|
| 1880 |
-
matched_indices = sorted({idx for lst in per_sprite_matched_indices for idx in lst})
|
| 1881 |
-
print("matched_indices------------------>", matched_indices)
|
| 1882 |
-
|
| 1883 |
for matched_idx in matched_indices:
|
| 1884 |
-
# defensive check
|
| 1885 |
if not (0 <= matched_idx < len(paths_list)):
|
| 1886 |
print(f" β matched_idx {matched_idx} out of range, skipping")
|
| 1887 |
continue
|
| 1888 |
-
|
| 1889 |
matched_image_path = paths_list[matched_idx]
|
| 1890 |
-
matched_path_p = Path(matched_image_path).resolve(strict=False)
|
| 1891 |
-
matched_folder_p = matched_path_p.parent
|
| 1892 |
matched_filename = matched_path_p.name
|
| 1893 |
-
|
| 1894 |
-
# Prepare display-only string (do NOT reassign matched_folder_p)
|
| 1895 |
matched_folder_display = display_like_windows_no_lead(matched_folder_p)
|
| 1896 |
-
|
| 1897 |
print(f"Processing matched image: {matched_image_path}")
|
| 1898 |
print(f" - Folder: {matched_folder_display}")
|
| 1899 |
-
|
| 1900 |
-
print(f" - Backdrop path: {display_like_windows_no_lead(backdrop_base_p)}")
|
| 1901 |
-
print(f" - Filename: {matched_filename}")
|
| 1902 |
-
|
| 1903 |
-
# Use a canonical string to store in the copied set (POSIX absolute-ish)
|
| 1904 |
folder_key = matched_folder_p.as_posix()
|
| 1905 |
-
|
| 1906 |
-
#
|
| 1907 |
if is_subpath(matched_folder_p, sprite_base_p) and folder_key not in copied_sprite_folders:
|
| 1908 |
print(f"Processing SPRITE folder: {matched_folder_display}")
|
| 1909 |
copied_sprite_folders.add(folder_key)
|
| 1910 |
-
|
| 1911 |
sprite_json_path = matched_folder_p / "sprite.json"
|
| 1912 |
-
print("sprite_json_path----------------------->", sprite_json_path)
|
| 1913 |
-
print("copied sprite folder----------------------->", copied_sprite_folders)
|
| 1914 |
if sprite_json_path.exists() and sprite_json_path.is_file():
|
| 1915 |
try:
|
| 1916 |
with sprite_json_path.open("r", encoding="utf-8") as f:
|
|
@@ -1921,19 +1917,15 @@ def similarity_matching(sprites_data: dict, project_folder: str, top_k: int = 1,
|
|
| 1921 |
print(f" β Failed to read sprite.json in {matched_folder_display}: {repr(e)}")
|
| 1922 |
else:
|
| 1923 |
print(f" β No sprite.json in {matched_folder_display}")
|
| 1924 |
-
|
| 1925 |
-
# copy non-matching files from the sprite folder (except matched image and sprite.json)
|
| 1926 |
try:
|
| 1927 |
sprite_files = list(matched_folder_p.iterdir())
|
| 1928 |
except Exception as e:
|
| 1929 |
sprite_files = []
|
| 1930 |
print(f" β Failed to list files in {matched_folder_display}: {repr(e)}")
|
| 1931 |
-
|
| 1932 |
print(f" Files in sprite folder: {[p.name for p in sprite_files]}")
|
| 1933 |
for p in sprite_files:
|
| 1934 |
fname = p.name
|
| 1935 |
if fname in (matched_filename, "sprite.json"):
|
| 1936 |
-
print(f" Skipping {fname} (matched image or sprite.json)")
|
| 1937 |
continue
|
| 1938 |
if p.is_file():
|
| 1939 |
dst = project_folder_p / fname
|
|
@@ -1942,17 +1934,11 @@ def similarity_matching(sprites_data: dict, project_folder: str, top_k: int = 1,
|
|
| 1942 |
print(f" β Copied sprite asset: {p} -> {dst}")
|
| 1943 |
except Exception as e:
|
| 1944 |
print(f" β Failed to copy sprite asset {p}: {repr(e)}")
|
| 1945 |
-
|
| 1946 |
-
|
| 1947 |
-
|
| 1948 |
-
# ---------- BACKDROP ----------
|
| 1949 |
if is_subpath(matched_folder_p, backdrop_base_p) and folder_key not in copied_backdrop_folders:
|
| 1950 |
print(f"Processing BACKDROP folder: {matched_folder_display}")
|
| 1951 |
copied_backdrop_folders.add(folder_key)
|
| 1952 |
-
print("backdrop_base_path----------------------->", display_like_windows_no_lead(backdrop_base_p))
|
| 1953 |
-
print("copied backdrop folder----------------------->", copied_backdrop_folders)
|
| 1954 |
-
|
| 1955 |
-
# copy matched backdrop image
|
| 1956 |
backdrop_src = matched_folder_p / matched_filename
|
| 1957 |
backdrop_dst = project_folder_p / matched_filename
|
| 1958 |
if backdrop_src.exists() and backdrop_src.is_file():
|
|
@@ -1963,19 +1949,15 @@ def similarity_matching(sprites_data: dict, project_folder: str, top_k: int = 1,
|
|
| 1963 |
print(f" β Failed to copy matched backdrop image {backdrop_src}: {repr(e)}")
|
| 1964 |
else:
|
| 1965 |
print(f" β Matched backdrop source not found: {backdrop_src}")
|
| 1966 |
-
|
| 1967 |
-
# copy other files from folder (skip project.json and matched image)
|
| 1968 |
try:
|
| 1969 |
backdrop_files = list(matched_folder_p.iterdir())
|
| 1970 |
except Exception as e:
|
| 1971 |
backdrop_files = []
|
| 1972 |
print(f" β Failed to list files in {matched_folder_display}: {repr(e)}")
|
| 1973 |
-
|
| 1974 |
print(f" Files in backdrop folder: {[p.name for p in backdrop_files]}")
|
| 1975 |
for p in backdrop_files:
|
| 1976 |
fname = p.name
|
| 1977 |
if fname in (matched_filename, "project.json"):
|
| 1978 |
-
print(f" Skipping {fname} (matched image or project.json)")
|
| 1979 |
continue
|
| 1980 |
if p.is_file():
|
| 1981 |
dst = project_folder_p / fname
|
|
@@ -1984,28 +1966,18 @@ def similarity_matching(sprites_data: dict, project_folder: str, top_k: int = 1,
|
|
| 1984 |
print(f" β Copied backdrop asset: {p} -> {dst}")
|
| 1985 |
except Exception as e:
|
| 1986 |
print(f" β Failed to copy backdrop asset {p}: {repr(e)}")
|
| 1987 |
-
else:
|
| 1988 |
-
print(f" Skipping {fname} (not a file)")
|
| 1989 |
-
|
| 1990 |
-
# read project.json to extract Stage/targets
|
| 1991 |
pj = matched_folder_p / "project.json"
|
| 1992 |
if pj.exists() and pj.is_file():
|
| 1993 |
try:
|
| 1994 |
with pj.open("r", encoding="utf-8") as f:
|
| 1995 |
bd_json = json.load(f)
|
| 1996 |
-
stage_count = 0
|
| 1997 |
for tgt in bd_json.get("targets", []):
|
| 1998 |
if tgt.get("isStage"):
|
| 1999 |
backdrop_data.append(tgt)
|
| 2000 |
-
stage_count += 1
|
| 2001 |
-
print(f" β Successfully read project.json from {matched_folder_display}, found {stage_count} stage(s)")
|
| 2002 |
except Exception as e:
|
| 2003 |
print(f" β Failed to read project.json in {matched_folder_display}: {repr(e)}")
|
| 2004 |
-
|
| 2005 |
-
|
| 2006 |
-
|
| 2007 |
-
print("---")
|
| 2008 |
-
|
| 2009 |
final_project = {
|
| 2010 |
"targets": [], "monitors": [], "extensions": [],
|
| 2011 |
"meta": {
|
|
@@ -2014,25 +1986,18 @@ def similarity_matching(sprites_data: dict, project_folder: str, top_k: int = 1,
|
|
| 2014 |
"agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
|
| 2015 |
}
|
| 2016 |
}
|
| 2017 |
-
|
| 2018 |
-
|
| 2019 |
-
# Add sprite targets (non-stage)
|
| 2020 |
for spr in project_data:
|
| 2021 |
if not spr.get("isStage", False):
|
| 2022 |
final_project["targets"].append(spr)
|
| 2023 |
-
|
| 2024 |
-
# then backdrop as the Stage
|
| 2025 |
if backdrop_data:
|
| 2026 |
all_costumes, sounds = [], []
|
| 2027 |
seen_costumes = set()
|
| 2028 |
for i, bd in enumerate(backdrop_data):
|
| 2029 |
for costume in bd.get("costumes", []):
|
| 2030 |
-
# Create a unique key for the costume
|
| 2031 |
key = (costume.get("name"), costume.get("assetId"))
|
| 2032 |
if key not in seen_costumes:
|
| 2033 |
seen_costumes.add(key)
|
| 2034 |
all_costumes.append(costume)
|
| 2035 |
-
|
| 2036 |
if i == 0:
|
| 2037 |
sounds = bd.get("sounds", [])
|
| 2038 |
stage_obj={
|
|
@@ -2059,18 +2024,15 @@ def similarity_matching(sprites_data: dict, project_folder: str, top_k: int = 1,
|
|
| 2059 |
logger.warning("β οΈ No backdrop matched. Using default static backdrop.")
|
| 2060 |
default_backdrop_path = BACKDROP_DIR / "cd21514d0531fdffb22204e0ec5ed84a.svg"
|
| 2061 |
default_backdrop_name = "cd21514d0531fdffb22204e0ec5ed84a.svg"
|
| 2062 |
-
|
| 2063 |
default_backdrop_sound = BACKDROP_DIR / "83a9787d4cb6f3b7632b4ddfebf74367.wav"
|
| 2064 |
default_backdrop_sound_name = "cd21514d0531fdffb22204e0ec5ed84a.svg"
|
| 2065 |
try:
|
| 2066 |
shutil.copy2(default_backdrop_path, os.path.join(project_folder, default_backdrop_name))
|
| 2067 |
logger.info(f"β
Default backdrop copied to project: {default_backdrop_name}")
|
| 2068 |
-
|
| 2069 |
shutil.copy2(default_backdrop_sound, os.path.join(project_folder, default_backdrop_sound_name))
|
| 2070 |
logger.info(f"β
Default backdrop sound copied to project: {default_backdrop_sound_name}")
|
| 2071 |
except Exception as e:
|
| 2072 |
logger.error(f"β Failed to copy default backdrop: {e}")
|
| 2073 |
-
|
| 2074 |
stage_obj={
|
| 2075 |
"isStage": True,
|
| 2076 |
"name": "Stage",
|
|
@@ -2115,6 +2077,694 @@ def similarity_matching(sprites_data: dict, project_folder: str, top_k: int = 1,
|
|
| 2115 |
json.dump(final_project, f, indent=2)
|
| 2116 |
|
| 2117 |
return project_json_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2118 |
|
| 2119 |
|
| 2120 |
def convert_pdf_stream_to_images(pdf_stream: io.BytesIO, dpi=300):
|
|
|
|
| 8 |
from langchain_groq import ChatGroq
|
| 9 |
from langgraph.prebuilt import create_react_agent
|
| 10 |
from pdf2image import convert_from_path, convert_from_bytes
|
| 11 |
+
from typing import Dict, TypedDict, Optional, Any, List, Tuple
|
| 12 |
+
from collections import defaultdict
|
| 13 |
from langgraph.graph import StateGraph, END
|
| 14 |
import uuid
|
| 15 |
import shutil, time, functools
|
| 16 |
from io import BytesIO
|
| 17 |
from pathlib import Path
|
|
|
|
| 18 |
from utils.block_relation_builder import block_builder, separate_scripts, transform_logic_to_action_flow, analyze_opcode_counts
|
| 19 |
from difflib import get_close_matches
|
| 20 |
import torch
|
| 21 |
from transformers import AutoImageProcessor, AutoModel
|
|
|
|
|
|
|
| 22 |
import torch
|
| 23 |
import json
|
| 24 |
import cv2
|
|
|
|
| 25 |
from imagededup.methods import PHash
|
| 26 |
from image_match.goldberg import ImageSignature
|
| 27 |
+
import sys
|
| 28 |
+
import math
|
| 29 |
+
import hashlib
|
| 30 |
+
|
| 31 |
+
|
| 32 |
# DINOv2 model id
|
| 33 |
DINOV2_MODEL = "facebook/dinov2-small"
|
| 34 |
|
|
|
|
| 348 |
return float(np.dot(a, b))
|
| 349 |
|
| 350 |
|
| 351 |
+
# --------------------------
|
| 352 |
+
# Hybrid Selection of Best Match
|
| 353 |
+
# --------------------------
|
| 354 |
+
|
| 355 |
+
def run_query_search_flow(
|
| 356 |
+
query_path: Optional[str] = None,
|
| 357 |
+
query_b64: Optional[str] = None,
|
| 358 |
+
processed_dir: str = "./processed",
|
| 359 |
+
embeddings_dict: Dict[str, np.ndarray] = None,
|
| 360 |
+
hash_dict: Dict[str, Any] = None,
|
| 361 |
+
signature_obj_map: Dict[str, Any] = None,
|
| 362 |
+
gis: Any = None,
|
| 363 |
+
phash: Any = None,
|
| 364 |
+
MAX_PHASH_BITS: int = 64,
|
| 365 |
+
k: int = 10,
|
| 366 |
+
) -> Tuple[
|
| 367 |
+
List[Tuple[str, float]],
|
| 368 |
+
List[Tuple[str, Any, float]],
|
| 369 |
+
List[Tuple[str, Any, float]],
|
| 370 |
+
List[Tuple[str, float, float, float, float]],
|
| 371 |
+
]:
|
| 372 |
+
"""
|
| 373 |
+
Run the full query/search flow (base64 -> preprocess -> embed -> scoring).
|
| 374 |
+
Accepts either query_path (file on disk) OR query_b64 (base64 string). If both are
|
| 375 |
+
provided, query_b64 takes precedence.
|
| 376 |
+
|
| 377 |
+
Returns:
|
| 378 |
+
embedding_results_sorted,
|
| 379 |
+
phash_results_sorted,
|
| 380 |
+
imgmatch_results_sorted,
|
| 381 |
+
combined_results_sorted
|
| 382 |
+
"""
|
| 383 |
+
|
| 384 |
+
# Validate inputs
|
| 385 |
+
if (query_path is None or query_path == "") and (query_b64 is None or query_b64 == ""):
|
| 386 |
+
raise ValueError("Either query_path or query_b64 must be provided.")
|
| 387 |
+
|
| 388 |
+
# Ensure processed_dir exists
|
| 389 |
+
os.makedirs(processed_dir, exist_ok=True)
|
| 390 |
+
|
| 391 |
+
print("\n--- Query/Search Phase ---")
|
| 392 |
+
|
| 393 |
+
# 1) Load query image (prefer base64 if provided)
|
| 394 |
+
if query_b64:
|
| 395 |
+
# base64 provided directly -> decode to PIL
|
| 396 |
+
query_from_b64 = base64_to_pil(query_b64)
|
| 397 |
+
if query_from_b64 is None:
|
| 398 |
+
raise RuntimeError("Could not decode provided base64 query. Exiting.")
|
| 399 |
+
query_pil_orig = query_from_b64
|
| 400 |
+
else:
|
| 401 |
+
# load from disk
|
| 402 |
+
if not os.path.exists(query_path):
|
| 403 |
+
raise FileNotFoundError(f"Query image not found: {query_path}")
|
| 404 |
+
query_pil_orig = load_image_pil(query_path)
|
| 405 |
+
if query_pil_orig is None:
|
| 406 |
+
raise RuntimeError("Could not load query image from path. Exiting.")
|
| 407 |
+
|
| 408 |
+
# also create a base64 roundtrip for robustness (keep original behaviour)
|
| 409 |
+
try:
|
| 410 |
+
query_b64 = pil_to_base64(query_pil_orig, fmt="PNG")
|
| 411 |
+
except Exception as e:
|
| 412 |
+
raise RuntimeError(f"Could not base64 query from disk image: {e}")
|
| 413 |
+
# keep decoded copy for consistency
|
| 414 |
+
query_from_b64 = base64_to_pil(query_b64)
|
| 415 |
+
if query_from_b64 is None:
|
| 416 |
+
raise RuntimeError("Could not decode query base64 after roundtrip. Exiting.")
|
| 417 |
+
|
| 418 |
+
# At this point, query_from_b64 is a PIL.Image we can continue with
|
| 419 |
+
# 2) Preprocess with OpenCV enhancement (best-effort; fallback to base64-decoded image)
|
| 420 |
+
enhanced_query_pil = process_image_cv2_from_pil(query_from_b64, scale=2)
|
| 421 |
+
if enhanced_query_pil is None:
|
| 422 |
+
print("[Query] OpenCV enhancement failed; falling back to base64-decoded image.")
|
| 423 |
+
enhanced_query_pil = query_from_b64
|
| 424 |
+
|
| 425 |
+
# Save the enhanced query (best-effort)
|
| 426 |
+
query_enhanced_path = os.path.join(processed_dir, "query_enhanced.png")
|
| 427 |
+
try:
|
| 428 |
+
enhanced_query_pil.save(query_enhanced_path, format="PNG")
|
| 429 |
+
except Exception:
|
| 430 |
+
try:
|
| 431 |
+
enhanced_query_pil.convert("RGB").save(query_enhanced_path, format="PNG")
|
| 432 |
+
except Exception:
|
| 433 |
+
print("[Warning] Could not save enhanced query image for inspection.")
|
| 434 |
+
|
| 435 |
+
# 3) Query embedding (preprocess -> model)
|
| 436 |
+
prepped = preprocess_for_model(enhanced_query_pil)
|
| 437 |
+
query_emb = get_dinov2_embedding_from_pil(prepped)
|
| 438 |
+
if query_emb is None:
|
| 439 |
+
raise RuntimeError("Could not compute query embedding. Exiting.")
|
| 440 |
+
|
| 441 |
+
# 4) Query phash computation
|
| 442 |
+
query_hash_arr = preprocess_for_hash(enhanced_query_pil)
|
| 443 |
+
if query_hash_arr is None:
|
| 444 |
+
raise RuntimeError("Could not compute query phash array. Exiting.")
|
| 445 |
+
query_phash = phash.encode_image(image_array=query_hash_arr)
|
| 446 |
+
|
| 447 |
+
# 5) Query signature generation (best-effort)
|
| 448 |
+
query_sig = None
|
| 449 |
+
query_sig_path = os.path.join(processed_dir, "query_for_sig.png")
|
| 450 |
+
try:
|
| 451 |
+
enhanced_query_pil.save(query_sig_path, format="PNG")
|
| 452 |
+
except Exception:
|
| 453 |
+
try:
|
| 454 |
+
enhanced_query_pil.convert("RGB").save(query_sig_path, format="PNG")
|
| 455 |
+
except Exception:
|
| 456 |
+
query_sig_path = None
|
| 457 |
+
|
| 458 |
+
if query_sig_path:
|
| 459 |
+
try:
|
| 460 |
+
query_sig = gis.generate_signature(query_sig_path)
|
| 461 |
+
except Exception as e:
|
| 462 |
+
print(f"[ImageSignature] failed for query: {e}")
|
| 463 |
+
query_sig = None
|
| 464 |
+
|
| 465 |
+
# -----------------------
|
| 466 |
+
# Prepare stored data arrays
|
| 467 |
+
# -----------------------
|
| 468 |
+
embeddings_dict = embeddings_dict or {}
|
| 469 |
+
hash_dict = hash_dict or {}
|
| 470 |
+
signature_obj_map = signature_obj_map or {}
|
| 471 |
+
|
| 472 |
+
image_paths = list(embeddings_dict.keys())
|
| 473 |
+
image_embeddings = np.array(list(embeddings_dict.values()), dtype=float) if embeddings_dict else np.array([])
|
| 474 |
+
|
| 475 |
+
def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
|
| 476 |
+
try:
|
| 477 |
+
return float(np.dot(a, b))
|
| 478 |
+
except Exception:
|
| 479 |
+
return -1.0
|
| 480 |
+
|
| 481 |
+
# Collections
|
| 482 |
+
embedding_results: List[Tuple[str, float]] = []
|
| 483 |
+
phash_results: List[Tuple[str, Any, float]] = []
|
| 484 |
+
imgmatch_results: List[Tuple[str, Any, float]] = []
|
| 485 |
+
combined_results: List[Tuple[str, float, float, float, float]] = []
|
| 486 |
+
|
| 487 |
+
# Iterate stored images and compute similarities
|
| 488 |
+
for idx, path in enumerate(image_paths):
|
| 489 |
+
# Embedding similarity
|
| 490 |
+
try:
|
| 491 |
+
stored_emb = image_embeddings[idx]
|
| 492 |
+
emb_sim = cosine_similarity(query_emb, stored_emb)
|
| 493 |
+
except Exception:
|
| 494 |
+
emb_sim = -1.0
|
| 495 |
+
embedding_results.append((path, emb_sim))
|
| 496 |
+
|
| 497 |
+
# PHash similarity (Hamming -> normalized sim)
|
| 498 |
+
try:
|
| 499 |
+
stored_ph = hash_dict.get(path)
|
| 500 |
+
if stored_ph is not None:
|
| 501 |
+
hd = phash.hamming_distance(query_phash, stored_ph)
|
| 502 |
+
ph_sim = max(0.0, 1.0 - (hd / float(MAX_PHASH_BITS)))
|
| 503 |
+
else:
|
| 504 |
+
hd = None
|
| 505 |
+
ph_sim = 0.0
|
| 506 |
+
except Exception:
|
| 507 |
+
hd = None
|
| 508 |
+
ph_sim = 0.0
|
| 509 |
+
phash_results.append((path, hd, ph_sim))
|
| 510 |
+
|
| 511 |
+
# Image signature similarity (normalized distance -> similarity)
|
| 512 |
+
try:
|
| 513 |
+
stored_sig = signature_obj_map.get(path)
|
| 514 |
+
if stored_sig is not None and query_sig is not None:
|
| 515 |
+
dist = gis.normalized_distance(stored_sig, query_sig)
|
| 516 |
+
im_sim = max(0.0, 1.0 - dist)
|
| 517 |
+
else:
|
| 518 |
+
dist = None
|
| 519 |
+
im_sim = 0.0
|
| 520 |
+
except Exception:
|
| 521 |
+
dist = None
|
| 522 |
+
im_sim = 0.0
|
| 523 |
+
imgmatch_results.append((path, dist, im_sim))
|
| 524 |
+
|
| 525 |
+
# Combined score: average of the three (embedding is clamped into [0,1])
|
| 526 |
+
emb_clamped = max(0.0, min(1.0, emb_sim))
|
| 527 |
+
combined = (emb_clamped + ph_sim + im_sim) / 3.0
|
| 528 |
+
combined_results.append((path, combined, emb_clamped, ph_sim, im_sim))
|
| 529 |
+
|
| 530 |
+
# -----------------------
|
| 531 |
+
# Sort results
|
| 532 |
+
# -----------------------
|
| 533 |
+
embedding_results.sort(key=lambda x: x[1], reverse=True)
|
| 534 |
+
phash_results_sorted = sorted(phash_results, key=lambda x: (x[2] is not None, x[2]), reverse=True)
|
| 535 |
+
imgmatch_results_sorted = sorted(imgmatch_results, key=lambda x: (x[2] is not None, x[2]), reverse=True)
|
| 536 |
+
combined_results.sort(key=lambda x: x[1], reverse=True)
|
| 537 |
+
|
| 538 |
+
# -----------------------
|
| 539 |
+
# Print Top-K results
|
| 540 |
+
# -----------------------
|
| 541 |
+
print("\nTop results by DINOv2 Embeddings:")
|
| 542 |
+
for i, (path, score) in enumerate(embedding_results[:k], start=1):
|
| 543 |
+
print(f"Rank {i}: {path} | Cosine: {score:.4f}")
|
| 544 |
+
|
| 545 |
+
print("\nTop results by PHash (Hamming distance & normalized sim):")
|
| 546 |
+
for i, (path, hd, sim) in enumerate(phash_results_sorted[:k], start=1):
|
| 547 |
+
print(f"Rank {i}: {path} | Hamming: {hd} | NormSim: {sim:.4f}")
|
| 548 |
+
|
| 549 |
+
print("\nTop results by ImageSignature (normalized similarity = 1 - distance):")
|
| 550 |
+
for i, (path, dist, sim) in enumerate(imgmatch_results_sorted[:k], start=1):
|
| 551 |
+
print(f"Rank {i}: {path} | NormDist: {dist} | NormSim: {sim:.4f}")
|
| 552 |
+
|
| 553 |
+
print("\nTop results by Combined Score (avg of embedding|phash|image-match):")
|
| 554 |
+
for i, (path, combined, emb_clamped, ph_sim, im_sim) in enumerate(combined_results[:k], start=1):
|
| 555 |
+
print(f"Rank {i}: {path} | Combined: {combined:.4f} | emb: {emb_clamped:.4f} | phash_sim: {ph_sim:.4f} | imgmatch_sim: {im_sim:.4f}")
|
| 556 |
+
|
| 557 |
+
print("\nSearch complete.")
|
| 558 |
+
|
| 559 |
+
# Return sorted lists for programmatic consumption
|
| 560 |
+
return embedding_results, phash_results_sorted, imgmatch_results_sorted, combined_results
|
| 561 |
+
|
| 562 |
# --------------------------
|
| 563 |
# Choose best candidate helper
|
| 564 |
# --------------------------
|
| 565 |
from collections import defaultdict
|
| 566 |
import math
|
| 567 |
|
| 568 |
+
def choose_top_candidates(embedding_results, phash_results, imgmatch_results, top_k=10,
|
| 569 |
+
method_weights=(0.5, 0.3, 0.2), verbose=True):
|
| 570 |
"""
|
| 571 |
embedding_results: list of (path, emb_sim) where emb_sim roughly in [-1,1] (we'll clamp to 0..1)
|
| 572 |
phash_results: list of (path, hamming, ph_sim) where ph_sim in [0,1]
|
|
|
|
| 1597 |
state["processing"]= False
|
| 1598 |
return state
|
| 1599 |
|
| 1600 |
+
# def extract_images_from_pdf(pdf_stream: io.BytesIO):
|
| 1601 |
+
# ''' Extract images from PDF and generate structured sprite JSON '''
|
| 1602 |
+
# manipulated_json = {}
|
| 1603 |
+
# img_elements = []
|
| 1604 |
+
# try:
|
| 1605 |
+
|
| 1606 |
+
# if isinstance(pdf_stream, io.BytesIO):
|
| 1607 |
+
# # use a random ID since there's no filename
|
| 1608 |
+
# pdf_id = uuid.uuid4().hex
|
| 1609 |
+
# else:
|
| 1610 |
+
# pdf_id = os.path.splitext(os.path.basename(pdf_stream))[0]
|
| 1611 |
+
|
| 1612 |
+
# try:
|
| 1613 |
+
# elements = partition_pdf(
|
| 1614 |
+
# file=pdf_stream,
|
| 1615 |
+
# strategy="hi_res",
|
| 1616 |
+
# # strategy="fast",
|
| 1617 |
+
# extract_image_block_types=["Image"],
|
| 1618 |
+
# hi_res_model_name="yolox",
|
| 1619 |
+
# extract_image_block_to_payload=True,
|
| 1620 |
+
# )
|
| 1621 |
+
# print(f"ELEMENTS")
|
| 1622 |
+
# except Exception as e:
|
| 1623 |
+
# raise RuntimeError(
|
| 1624 |
+
# f"β Failed to extract images from PDF: {str(e)}")
|
| 1625 |
+
|
| 1626 |
+
# file_elements = [element.to_dict() for element in elements]
|
| 1627 |
+
# print(f"========== file elements: \n{file_elements}")
|
| 1628 |
+
|
| 1629 |
+
# sprite_count = 1
|
| 1630 |
+
# for el in file_elements:
|
| 1631 |
+
# img_b64 = el["metadata"].get("image_base64")
|
| 1632 |
+
# if not img_b64:
|
| 1633 |
+
# continue
|
| 1634 |
+
|
| 1635 |
+
# manipulated_json[f"Sprite {sprite_count}"] = {
|
| 1636 |
+
# "base64": el["metadata"]["image_base64"],
|
| 1637 |
+
# "file-path": pdf_id,
|
| 1638 |
+
# }
|
| 1639 |
+
# sprite_count += 1
|
| 1640 |
+
# return manipulated_json
|
| 1641 |
+
# except Exception as e:
|
| 1642 |
+
# raise RuntimeError(f"β Error in extract_images_from_pdf: {str(e)}")
|
| 1643 |
+
|
| 1644 |
+
def extract_images_from_pdf(pdf_stream, output_dir):
|
| 1645 |
manipulated_json = {}
|
|
|
|
| 1646 |
try:
|
| 1647 |
+
pdf_id = uuid.uuid4().hex
|
| 1648 |
+
elements = partition_pdf(
|
| 1649 |
+
file=pdf_stream,
|
| 1650 |
+
strategy="hi_res",
|
| 1651 |
+
extract_image_block_types=["Image"],
|
| 1652 |
+
hi_res_model_name="yolox",
|
| 1653 |
+
extract_image_block_to_payload=False,
|
| 1654 |
+
extract_image_block_output_dir=BLOCKS_DIR,
|
| 1655 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1656 |
file_elements = [element.to_dict() for element in elements]
|
|
|
|
|
|
|
| 1657 |
sprite_count = 1
|
| 1658 |
for el in file_elements:
|
| 1659 |
+
img_path = el["metadata"].get("image_path")
|
| 1660 |
+
|
| 1661 |
+
# β
skip if no image_path was returned
|
| 1662 |
+
if not img_path:
|
| 1663 |
continue
|
| 1664 |
+
|
| 1665 |
+
with open(img_path, "rb") as f:
|
| 1666 |
+
base_file = base64.b64encode(f.read()).decode("utf-8")
|
| 1667 |
+
|
| 1668 |
+
image_uuid = str(uuid.uuid4())
|
| 1669 |
manipulated_json[f"Sprite {sprite_count}"] = {
|
| 1670 |
+
"base64": base_file,
|
| 1671 |
+
"file-path": img_path,
|
| 1672 |
+
"pdf-id": pdf_id,
|
| 1673 |
+
"image-uuid": image_uuid,
|
| 1674 |
}
|
| 1675 |
+
|
| 1676 |
sprite_count += 1
|
| 1677 |
+
|
| 1678 |
return manipulated_json
|
| 1679 |
except Exception as e:
|
| 1680 |
raise RuntimeError(f"β Error in extract_images_from_pdf: {str(e)}")
|
| 1681 |
+
|
| 1682 |
+
|
| 1683 |
+
|
| 1684 |
def similarity_matching(sprites_data: dict, project_folder: str, top_k: int = 1, min_similarity: float = None) -> str:
|
| 1685 |
print("π Running similarity matchingβ¦")
|
|
|
|
|
|
|
| 1686 |
os.makedirs(project_folder, exist_ok=True)
|
| 1687 |
|
| 1688 |
+
backdrop_base_path = r"D:\DEV PATEL\2025\scratch_VLM\scratch_agent\blocks\Backdrops"
|
| 1689 |
+
sprite_base_path = r"D:\DEV PATEL\2025\scratch_VLM\scratch_agent\blocks\sprites"
|
| 1690 |
+
code_blocks_path = r"D:\DEV PATEL\2025\scratch_VLM\scratch_agent\blocks\code_blocks"
|
| 1691 |
+
# out_path = r"D:\DEV PATEL\2025\scratch_VLM\scratch_agent\blocks\out_json"
|
| 1692 |
+
|
| 1693 |
|
| 1694 |
project_json_path = os.path.join(project_folder, "project.json")
|
| 1695 |
|
|
|
|
| 1702 |
sprite_base64.append(sprite["base64"])
|
| 1703 |
|
| 1704 |
sprite_images_bytes = []
|
| 1705 |
+
sprite_b64_clean = [] # <<< new: store cleaned base64 strings
|
| 1706 |
for b64 in sprite_base64:
|
| 1707 |
+
# remove possible "data:image/..;base64," prefix
|
| 1708 |
+
raw_b64 = b64.split(",")[-1]
|
| 1709 |
+
sprite_b64_clean.append(raw_b64)
|
| 1710 |
+
|
| 1711 |
+
# decode into BytesIO for local processing
|
| 1712 |
+
img = Image.open(BytesIO(base64.b64decode(raw_b64))).convert("RGB")
|
| 1713 |
buffer = BytesIO()
|
| 1714 |
img.save(buffer, format="PNG")
|
| 1715 |
buffer.seek(0)
|
| 1716 |
sprite_images_bytes.append(buffer)
|
| 1717 |
+
|
| 1718 |
+
def hybrid_similarity_matching(sprite_images_bytes, sprite_ids, min_similarity=None, top_k=5, method_weights=(0.5,0.3,0.2)):
|
| 1719 |
+
from PIL import Image
|
| 1720 |
+
# Local safe defaults
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1721 |
embeddings_path = os.path.join(BLOCKS_DIR, "hybrid_embeddings.json")
|
| 1722 |
+
hash_path = os.path.join(BLOCKS_DIR, "phash_data.json")
|
| 1723 |
signature_path = os.path.join(BLOCKS_DIR, "signature_data.json")
|
| 1724 |
+
|
| 1725 |
# Load embeddings
|
| 1726 |
+
embedding_json = {}
|
| 1727 |
+
if os.path.exists(embeddings_path):
|
| 1728 |
+
with open(embeddings_path, "r", encoding="utf-8") as f:
|
| 1729 |
+
embedding_json = json.load(f)
|
| 1730 |
+
|
| 1731 |
+
# Load phash data (if exists) -> ensure hash_dict variable exists
|
| 1732 |
hash_dict = {}
|
| 1733 |
if os.path.exists(hash_path):
|
| 1734 |
+
try:
|
| 1735 |
+
with open(hash_path, "r", encoding="utf-8") as f:
|
| 1736 |
+
hash_data = json.load(f)
|
| 1737 |
+
for path, hash_str in hash_data.items():
|
| 1738 |
+
try:
|
| 1739 |
+
hash_dict[path] = hash_str
|
| 1740 |
+
except Exception:
|
| 1741 |
+
pass
|
| 1742 |
+
except Exception:
|
| 1743 |
+
pass
|
| 1744 |
+
|
| 1745 |
+
# Load signature data (if exists) -> ensure signature_dict exists
|
| 1746 |
signature_dict = {}
|
| 1747 |
+
sig_data = {}
|
| 1748 |
if os.path.exists(signature_path):
|
| 1749 |
+
try:
|
| 1750 |
+
with open(signature_path, "r", encoding="utf-8") as f:
|
| 1751 |
+
sig_data = json.load(f)
|
| 1752 |
+
for path, sig_list in sig_data.items():
|
| 1753 |
+
try:
|
| 1754 |
+
signature_dict[path] = np.array(sig_list)
|
| 1755 |
+
except Exception:
|
| 1756 |
+
pass
|
| 1757 |
+
except Exception:
|
| 1758 |
+
pass
|
| 1759 |
+
|
| 1760 |
+
# Parse embeddings into lists
|
| 1761 |
paths_list = []
|
| 1762 |
embeddings_list = []
|
|
|
|
| 1763 |
if isinstance(embedding_json, dict):
|
| 1764 |
for p, emb in embedding_json.items():
|
| 1765 |
if isinstance(emb, dict):
|
|
|
|
| 1783 |
continue
|
| 1784 |
paths_list.append(os.path.normpath(str(p)))
|
| 1785 |
embeddings_list.append(np.asarray(emb, dtype=np.float32))
|
| 1786 |
+
|
| 1787 |
if len(paths_list) == 0:
|
| 1788 |
+
print("β No reference images/embeddings found (this test harness may be running without data)")
|
| 1789 |
+
# Return empty results gracefully
|
| 1790 |
+
return [[] for _ in sprite_images_bytes], [[] for _ in sprite_images_bytes], []
|
| 1791 |
+
|
| 1792 |
ref_matrix = np.vstack(embeddings_list).astype(np.float32)
|
| 1793 |
|
| 1794 |
+
# Batch: Get all sprite embeddings, phash, sigs first
|
| 1795 |
+
sprite_emb_list = []
|
| 1796 |
+
sprite_phash_list = []
|
| 1797 |
+
sprite_sig_list = []
|
| 1798 |
+
per_sprite_final_indices = []
|
| 1799 |
+
per_sprite_final_scores = []
|
| 1800 |
+
per_sprite_rerank_debug = []
|
| 1801 |
+
for i, sprite_bytes in enumerate(sprite_images_bytes):
|
|
|
|
| 1802 |
sprite_pil = Image.open(sprite_bytes)
|
| 1803 |
+
enhanced_sprite = process_image_cv2_from_pil(sprite_pil, scale=2) or sprite_pil
|
| 1804 |
+
# sprite_emb = get_dinov2_embedding_from_pil(preprocess_for_model(enhanced_sprite)) or np.zeros(ref_matrix.shape[1])
|
| 1805 |
+
# sprite_emb_list.append(sprite_emb)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1806 |
sprite_emb = get_dinov2_embedding_from_pil(preprocess_for_model(enhanced_sprite))
|
| 1807 |
+
sprite_emb = sprite_emb if sprite_emb is not None else np.zeros(ref_matrix.shape[1])
|
| 1808 |
+
sprite_emb_list.append(sprite_emb)
|
| 1809 |
+
# Perceptual hash
|
|
|
|
| 1810 |
sprite_hash_arr = preprocess_for_hash(enhanced_sprite)
|
| 1811 |
sprite_phash = None
|
| 1812 |
if sprite_hash_arr is not None:
|
| 1813 |
+
try: sprite_phash = phash.encode_image(image_array=sprite_hash_arr)
|
| 1814 |
+
except: pass
|
| 1815 |
+
sprite_phash_list.append(sprite_phash)
|
| 1816 |
+
# Signature
|
|
|
|
|
|
|
| 1817 |
sprite_sig = None
|
| 1818 |
+
embedding_results, phash_results, imgmatch_results, combined_results = run_query_search_flow(
|
| 1819 |
+
query_b64=sprite_b64_clean[i],
|
| 1820 |
+
processed_dir=BLOCKS_DIR,
|
| 1821 |
+
embeddings_dict=embedding_json,
|
| 1822 |
+
hash_dict=hash_data,
|
| 1823 |
+
signature_obj_map=sig_data,
|
| 1824 |
+
gis=gis,
|
| 1825 |
+
phash=phash,
|
| 1826 |
+
MAX_PHASH_BITS=64,
|
| 1827 |
+
k=5
|
| 1828 |
+
)
|
| 1829 |
+
# Call the advanced re-ranker
|
| 1830 |
+
rerank_result = choose_top_candidates(embedding_results, phash_results, imgmatch_results,
|
| 1831 |
+
top_k=top_k, method_weights=method_weights, verbose=True)
|
| 1832 |
+
per_sprite_rerank_debug.append(rerank_result)
|
| 1833 |
+
|
| 1834 |
+
# Selection logic: prefer consensus, else weighted top-1
|
| 1835 |
+
final = None
|
| 1836 |
+
if len(rerank_result["consensus_topk"]) > 0:
|
| 1837 |
+
consensus = rerank_result["consensus_topk"]
|
| 1838 |
+
best = max(consensus, key=lambda p: rerank_result["weighted_scores_full"].get(p, 0.0))
|
| 1839 |
+
final = best
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1840 |
else:
|
| 1841 |
+
final = rerank_result["weighted_topk"][0][0] if rerank_result["weighted_topk"] else None
|
| 1842 |
+
|
| 1843 |
+
# Store index and score for downstream use
|
| 1844 |
+
if final is not None and final in paths_list:
|
| 1845 |
+
idx = paths_list.index(final)
|
| 1846 |
+
score = rerank_result["weighted_scores_full"].get(final, 0.0)
|
| 1847 |
+
per_sprite_final_indices.append([idx])
|
| 1848 |
+
per_sprite_final_scores.append([score])
|
| 1849 |
+
print(f"Sprite '{sprite_ids}' FINAL selected: {final} (index {idx}) score={score:.4f}")
|
| 1850 |
+
else:
|
| 1851 |
+
per_sprite_final_indices.append([])
|
| 1852 |
+
per_sprite_final_scores.append([])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1853 |
|
| 1854 |
+
return per_sprite_final_indices, per_sprite_final_scores, paths_list#, per_sprite_rerank_debug
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1855 |
|
|
|
|
|
|
|
|
|
|
| 1856 |
# Use hybrid matching system
|
|
|
|
| 1857 |
per_sprite_matched_indices, per_sprite_scores, paths_list = hybrid_similarity_matching(
|
| 1858 |
sprite_images_bytes, sprite_ids, min_similarity, top_k, method_weights=(0.5, 0.3, 0.2)
|
| 1859 |
)
|
|
|
|
| 1866 |
copied_sprite_folders = set()
|
| 1867 |
copied_backdrop_folders = set()
|
| 1868 |
|
|
|
|
| 1869 |
matched_indices = sorted({idx for lst in per_sprite_matched_indices for idx in lst})
|
| 1870 |
print("matched_indices------------------>",matched_indices)
|
| 1871 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1872 |
sprite_base_p = Path(sprite_base_path).resolve(strict=False)
|
| 1873 |
backdrop_base_p = Path(backdrop_base_path).resolve(strict=False)
|
| 1874 |
project_folder_p = Path(project_folder)
|
| 1875 |
project_folder_p.mkdir(parents=True, exist_ok=True)
|
| 1876 |
|
|
|
|
|
|
|
|
|
|
| 1877 |
def display_like_windows_no_lead(p: Path) -> str:
|
| 1878 |
+
s = p.as_posix()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1879 |
if s.startswith("/"):
|
| 1880 |
s = s[1:]
|
| 1881 |
return s.replace("/", "\\")
|
| 1882 |
|
| 1883 |
def is_subpath(child: Path, parent: Path) -> bool:
|
|
|
|
| 1884 |
try:
|
|
|
|
| 1885 |
child.relative_to(parent)
|
| 1886 |
return True
|
| 1887 |
except Exception:
|
| 1888 |
return False
|
| 1889 |
+
|
| 1890 |
+
# Copy assets and build project data (unchanged from your version)
|
|
|
|
|
|
|
|
|
|
| 1891 |
for matched_idx in matched_indices:
|
|
|
|
| 1892 |
if not (0 <= matched_idx < len(paths_list)):
|
| 1893 |
print(f" β matched_idx {matched_idx} out of range, skipping")
|
| 1894 |
continue
|
|
|
|
| 1895 |
matched_image_path = paths_list[matched_idx]
|
| 1896 |
+
matched_path_p = Path(matched_image_path).resolve(strict=False)
|
| 1897 |
+
matched_folder_p = matched_path_p.parent
|
| 1898 |
matched_filename = matched_path_p.name
|
|
|
|
|
|
|
| 1899 |
matched_folder_display = display_like_windows_no_lead(matched_folder_p)
|
|
|
|
| 1900 |
print(f"Processing matched image: {matched_image_path}")
|
| 1901 |
print(f" - Folder: {matched_folder_display}")
|
| 1902 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1903 |
folder_key = matched_folder_p.as_posix()
|
| 1904 |
+
|
| 1905 |
+
# SPRITE
|
| 1906 |
if is_subpath(matched_folder_p, sprite_base_p) and folder_key not in copied_sprite_folders:
|
| 1907 |
print(f"Processing SPRITE folder: {matched_folder_display}")
|
| 1908 |
copied_sprite_folders.add(folder_key)
|
|
|
|
| 1909 |
sprite_json_path = matched_folder_p / "sprite.json"
|
|
|
|
|
|
|
| 1910 |
if sprite_json_path.exists() and sprite_json_path.is_file():
|
| 1911 |
try:
|
| 1912 |
with sprite_json_path.open("r", encoding="utf-8") as f:
|
|
|
|
| 1917 |
print(f" β Failed to read sprite.json in {matched_folder_display}: {repr(e)}")
|
| 1918 |
else:
|
| 1919 |
print(f" β No sprite.json in {matched_folder_display}")
|
|
|
|
|
|
|
| 1920 |
try:
|
| 1921 |
sprite_files = list(matched_folder_p.iterdir())
|
| 1922 |
except Exception as e:
|
| 1923 |
sprite_files = []
|
| 1924 |
print(f" β Failed to list files in {matched_folder_display}: {repr(e)}")
|
|
|
|
| 1925 |
print(f" Files in sprite folder: {[p.name for p in sprite_files]}")
|
| 1926 |
for p in sprite_files:
|
| 1927 |
fname = p.name
|
| 1928 |
if fname in (matched_filename, "sprite.json"):
|
|
|
|
| 1929 |
continue
|
| 1930 |
if p.is_file():
|
| 1931 |
dst = project_folder_p / fname
|
|
|
|
| 1934 |
print(f" β Copied sprite asset: {p} -> {dst}")
|
| 1935 |
except Exception as e:
|
| 1936 |
print(f" β Failed to copy sprite asset {p}: {repr(e)}")
|
| 1937 |
+
|
| 1938 |
+
# BACKDROP
|
|
|
|
|
|
|
| 1939 |
if is_subpath(matched_folder_p, backdrop_base_p) and folder_key not in copied_backdrop_folders:
|
| 1940 |
print(f"Processing BACKDROP folder: {matched_folder_display}")
|
| 1941 |
copied_backdrop_folders.add(folder_key)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1942 |
backdrop_src = matched_folder_p / matched_filename
|
| 1943 |
backdrop_dst = project_folder_p / matched_filename
|
| 1944 |
if backdrop_src.exists() and backdrop_src.is_file():
|
|
|
|
| 1949 |
print(f" β Failed to copy matched backdrop image {backdrop_src}: {repr(e)}")
|
| 1950 |
else:
|
| 1951 |
print(f" β Matched backdrop source not found: {backdrop_src}")
|
|
|
|
|
|
|
| 1952 |
try:
|
| 1953 |
backdrop_files = list(matched_folder_p.iterdir())
|
| 1954 |
except Exception as e:
|
| 1955 |
backdrop_files = []
|
| 1956 |
print(f" β Failed to list files in {matched_folder_display}: {repr(e)}")
|
|
|
|
| 1957 |
print(f" Files in backdrop folder: {[p.name for p in backdrop_files]}")
|
| 1958 |
for p in backdrop_files:
|
| 1959 |
fname = p.name
|
| 1960 |
if fname in (matched_filename, "project.json"):
|
|
|
|
| 1961 |
continue
|
| 1962 |
if p.is_file():
|
| 1963 |
dst = project_folder_p / fname
|
|
|
|
| 1966 |
print(f" β Copied backdrop asset: {p} -> {dst}")
|
| 1967 |
except Exception as e:
|
| 1968 |
print(f" β Failed to copy backdrop asset {p}: {repr(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1969 |
pj = matched_folder_p / "project.json"
|
| 1970 |
if pj.exists() and pj.is_file():
|
| 1971 |
try:
|
| 1972 |
with pj.open("r", encoding="utf-8") as f:
|
| 1973 |
bd_json = json.load(f)
|
|
|
|
| 1974 |
for tgt in bd_json.get("targets", []):
|
| 1975 |
if tgt.get("isStage"):
|
| 1976 |
backdrop_data.append(tgt)
|
|
|
|
|
|
|
| 1977 |
except Exception as e:
|
| 1978 |
print(f" β Failed to read project.json in {matched_folder_display}: {repr(e)}")
|
| 1979 |
+
|
| 1980 |
+
# Final project JSON creation (same as your code)
|
|
|
|
|
|
|
|
|
|
| 1981 |
final_project = {
|
| 1982 |
"targets": [], "monitors": [], "extensions": [],
|
| 1983 |
"meta": {
|
|
|
|
| 1986 |
"agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
|
| 1987 |
}
|
| 1988 |
}
|
|
|
|
|
|
|
|
|
|
| 1989 |
for spr in project_data:
|
| 1990 |
if not spr.get("isStage", False):
|
| 1991 |
final_project["targets"].append(spr)
|
|
|
|
|
|
|
| 1992 |
if backdrop_data:
|
| 1993 |
all_costumes, sounds = [], []
|
| 1994 |
seen_costumes = set()
|
| 1995 |
for i, bd in enumerate(backdrop_data):
|
| 1996 |
for costume in bd.get("costumes", []):
|
|
|
|
| 1997 |
key = (costume.get("name"), costume.get("assetId"))
|
| 1998 |
if key not in seen_costumes:
|
| 1999 |
seen_costumes.add(key)
|
| 2000 |
all_costumes.append(costume)
|
|
|
|
| 2001 |
if i == 0:
|
| 2002 |
sounds = bd.get("sounds", [])
|
| 2003 |
stage_obj={
|
|
|
|
| 2024 |
logger.warning("β οΈ No backdrop matched. Using default static backdrop.")
|
| 2025 |
default_backdrop_path = BACKDROP_DIR / "cd21514d0531fdffb22204e0ec5ed84a.svg"
|
| 2026 |
default_backdrop_name = "cd21514d0531fdffb22204e0ec5ed84a.svg"
|
|
|
|
| 2027 |
default_backdrop_sound = BACKDROP_DIR / "83a9787d4cb6f3b7632b4ddfebf74367.wav"
|
| 2028 |
default_backdrop_sound_name = "cd21514d0531fdffb22204e0ec5ed84a.svg"
|
| 2029 |
try:
|
| 2030 |
shutil.copy2(default_backdrop_path, os.path.join(project_folder, default_backdrop_name))
|
| 2031 |
logger.info(f"β
Default backdrop copied to project: {default_backdrop_name}")
|
|
|
|
| 2032 |
shutil.copy2(default_backdrop_sound, os.path.join(project_folder, default_backdrop_sound_name))
|
| 2033 |
logger.info(f"β
Default backdrop sound copied to project: {default_backdrop_sound_name}")
|
| 2034 |
except Exception as e:
|
| 2035 |
logger.error(f"β Failed to copy default backdrop: {e}")
|
|
|
|
| 2036 |
stage_obj={
|
| 2037 |
"isStage": True,
|
| 2038 |
"name": "Stage",
|
|
|
|
| 2077 |
json.dump(final_project, f, indent=2)
|
| 2078 |
|
| 2079 |
return project_json_path
|
| 2080 |
+
# ''' It appends all the list and paths from json files and pick the best match's path'''
|
| 2081 |
+
# def similarity_matching(sprites_data: dict, project_folder: str, top_k: int = 1, min_similarity: float = None) -> str:
|
| 2082 |
+
# print("π Running similarity matchingβ¦")
|
| 2083 |
+
# import os
|
| 2084 |
+
# import json
|
| 2085 |
+
# os.makedirs(project_folder, exist_ok=True)
|
| 2086 |
+
|
| 2087 |
+
# backdrop_base_path = os.path.normpath(str(BACKDROP_DIR))
|
| 2088 |
+
# sprite_base_path = os.path.normpath(str(SPRITE_DIR))
|
| 2089 |
+
# code_blocks_path = os.path.normpath(str(CODE_BLOCKS_DIR))
|
| 2090 |
+
|
| 2091 |
+
# project_json_path = os.path.join(project_folder, "project.json")
|
| 2092 |
+
|
| 2093 |
+
# # -------------------------
|
| 2094 |
+
# # Build sprite images list (BytesIO) from sprites_data
|
| 2095 |
+
# # -------------------------
|
| 2096 |
+
# sprite_ids, sprite_base64 = [], []
|
| 2097 |
+
# for sid, sprite in sprites_data.items():
|
| 2098 |
+
# sprite_ids.append(sid)
|
| 2099 |
+
# sprite_base64.append(sprite["base64"])
|
| 2100 |
+
|
| 2101 |
+
# sprite_images_bytes = []
|
| 2102 |
+
# for b64 in sprite_base64:
|
| 2103 |
+
# img = Image.open(BytesIO(base64.b64decode(b64.split(",")[-1]))).convert("RGB")
|
| 2104 |
+
# buffer = BytesIO()
|
| 2105 |
+
# img.save(buffer, format="PNG")
|
| 2106 |
+
# buffer.seek(0)
|
| 2107 |
+
# sprite_images_bytes.append(buffer)
|
| 2108 |
+
|
| 2109 |
+
# # -----------------------------------------
|
| 2110 |
+
# # Hybrid Similarity Matching System
|
| 2111 |
+
# # -----------------------------------------
|
| 2112 |
+
# def hybrid_similarity_matching(sprite_images_bytes, sprite_ids,
|
| 2113 |
+
# min_similarity=None, top_k=5, method_weights=(0.5, 0.3, 0.2)):
|
| 2114 |
+
# """
|
| 2115 |
+
# Hybrid similarity matching using DINOv2 embeddings, perceptual hashing, and image signatures
|
| 2116 |
+
|
| 2117 |
+
# Args:
|
| 2118 |
+
# sprite_images_bytes: List of image bytes
|
| 2119 |
+
# sprite_ids: List of sprite identifiers
|
| 2120 |
+
# blocks_dir: Directory containing reference blocks
|
| 2121 |
+
# min_similarity: Minimum similarity threshold
|
| 2122 |
+
# top_k: Number of top matches to return
|
| 2123 |
+
# method_weights: Weights for (embedding, phash, image_signature) methods
|
| 2124 |
+
|
| 2125 |
+
# Returns:
|
| 2126 |
+
# per_sprite_matched_indices, per_sprite_scores, paths_list
|
| 2127 |
+
# """
|
| 2128 |
+
# import imagehash as phash
|
| 2129 |
+
# from image_match.goldberg import ImageSignature
|
| 2130 |
+
# import math
|
| 2131 |
+
# from collections import defaultdict
|
| 2132 |
+
|
| 2133 |
+
# # Load reference data
|
| 2134 |
+
# embeddings_path = os.path.join(BLOCKS_DIR, "hybrid_embeddings.json")
|
| 2135 |
+
# hash_path = os.path.join(BLOCKS_DIR, "phash_data.json")
|
| 2136 |
+
# signature_path = os.path.join(BLOCKS_DIR, "signature_data.json")
|
| 2137 |
+
|
| 2138 |
+
# # Load embeddings
|
| 2139 |
+
# with open(embeddings_path, "r", encoding="utf-8") as f:
|
| 2140 |
+
# embedding_json = json.load(f)
|
| 2141 |
+
|
| 2142 |
+
# # Load phash data (if exists)
|
| 2143 |
+
# hash_dict = {}
|
| 2144 |
+
# if os.path.exists(hash_path):
|
| 2145 |
+
# with open(hash_path, "r", encoding="utf-8") as f:
|
| 2146 |
+
# hash_data = json.load(f)
|
| 2147 |
+
# for path, hash_str in hash_data.items():
|
| 2148 |
+
# try:
|
| 2149 |
+
# hash_dict[path] = phash.hex_to_hash(hash_str)
|
| 2150 |
+
# except:
|
| 2151 |
+
# pass
|
| 2152 |
+
|
| 2153 |
+
# # Load signature data (if exists)
|
| 2154 |
+
# signature_dict = {}
|
| 2155 |
+
# gis = ImageSignature()
|
| 2156 |
+
# if os.path.exists(signature_path):
|
| 2157 |
+
# with open(signature_path, "r", encoding="utf-8") as f:
|
| 2158 |
+
# sig_data = json.load(f)
|
| 2159 |
+
# for path, sig_list in sig_data.items():
|
| 2160 |
+
# try:
|
| 2161 |
+
# signature_dict[path] = np.array(sig_list)
|
| 2162 |
+
# except:
|
| 2163 |
+
# pass
|
| 2164 |
+
|
| 2165 |
+
# # Parse embeddings
|
| 2166 |
+
# paths_list = []
|
| 2167 |
+
# embeddings_list = []
|
| 2168 |
+
|
| 2169 |
+
# if isinstance(embedding_json, dict):
|
| 2170 |
+
# for p, emb in embedding_json.items():
|
| 2171 |
+
# if isinstance(emb, dict):
|
| 2172 |
+
# maybe_emb = emb.get("embedding") or emb.get("embeddings") or emb.get("emb")
|
| 2173 |
+
# if maybe_emb is None:
|
| 2174 |
+
# continue
|
| 2175 |
+
# arr = np.asarray(maybe_emb, dtype=np.float32)
|
| 2176 |
+
# elif isinstance(emb, list):
|
| 2177 |
+
# arr = np.asarray(emb, dtype=np.float32)
|
| 2178 |
+
# else:
|
| 2179 |
+
# continue
|
| 2180 |
+
# paths_list.append(os.path.normpath(str(p)))
|
| 2181 |
+
# embeddings_list.append(arr)
|
| 2182 |
+
# elif isinstance(embedding_json, list):
|
| 2183 |
+
# for item in embedding_json:
|
| 2184 |
+
# if not isinstance(item, dict):
|
| 2185 |
+
# continue
|
| 2186 |
+
# p = item.get("path") or item.get("image_path") or item.get("file") or item.get("filename") or item.get("img_path")
|
| 2187 |
+
# emb = item.get("embeddings") or item.get("embedding") or item.get("features") or item.get("vector") or item.get("emb")
|
| 2188 |
+
# if p is None or emb is None:
|
| 2189 |
+
# continue
|
| 2190 |
+
# paths_list.append(os.path.normpath(str(p)))
|
| 2191 |
+
# embeddings_list.append(np.asarray(emb, dtype=np.float32))
|
| 2192 |
+
|
| 2193 |
+
# if len(paths_list) == 0:
|
| 2194 |
+
# raise RuntimeError("No reference images/embeddings found")
|
| 2195 |
+
|
| 2196 |
+
# ref_matrix = np.vstack(embeddings_list).astype(np.float32)
|
| 2197 |
+
|
| 2198 |
+
# # Process input sprites
|
| 2199 |
+
# # init_dinov2()
|
| 2200 |
+
# per_sprite_matched_indices = []
|
| 2201 |
+
# per_sprite_scores = []
|
| 2202 |
+
|
| 2203 |
+
# for i, (sprite_bytes, sprite_id) in enumerate(zip(sprite_images_bytes, sprite_ids)):
|
| 2204 |
+
# print(f"Processing sprite {i+1}/{len(sprite_ids)}: {sprite_id}")
|
| 2205 |
+
|
| 2206 |
+
# # Convert bytes to PIL for processing
|
| 2207 |
+
# sprite_pil = Image.open(sprite_bytes)
|
| 2208 |
+
# if sprite_pil is None:
|
| 2209 |
+
# per_sprite_matched_indices.append([])
|
| 2210 |
+
# per_sprite_scores.append([])
|
| 2211 |
+
# continue
|
| 2212 |
+
|
| 2213 |
+
# # Enhance image
|
| 2214 |
+
# enhanced_sprite = process_image_cv2_from_pil(sprite_pil, scale=2)
|
| 2215 |
+
# if enhanced_sprite is None:
|
| 2216 |
+
# enhanced_sprite = sprite_pil
|
| 2217 |
+
|
| 2218 |
+
# # 1. Compute DINOv2 embedding
|
| 2219 |
+
# sprite_emb = get_dinov2_embedding_from_pil(preprocess_for_model(enhanced_sprite))
|
| 2220 |
+
# if sprite_emb is None:
|
| 2221 |
+
# sprite_emb = np.zeros(ref_matrix.shape[1])
|
| 2222 |
+
|
| 2223 |
+
# # 2. Compute perceptual hash
|
| 2224 |
+
# sprite_hash_arr = preprocess_for_hash(enhanced_sprite)
|
| 2225 |
+
# sprite_phash = None
|
| 2226 |
+
# if sprite_hash_arr is not None:
|
| 2227 |
+
# try:
|
| 2228 |
+
# sprite_phash = phash.encode_image(image_array=sprite_hash_arr)
|
| 2229 |
+
# except:
|
| 2230 |
+
# pass
|
| 2231 |
+
|
| 2232 |
+
# # 3. Compute image signature
|
| 2233 |
+
# sprite_sig = None
|
| 2234 |
+
# try:
|
| 2235 |
+
# temp_path = f"temp_sprite_{i}.png"
|
| 2236 |
+
# enhanced_sprite.save(temp_path, format="PNG")
|
| 2237 |
+
# sprite_sig = gis.generate_signature(temp_path)
|
| 2238 |
+
# os.remove(temp_path)
|
| 2239 |
+
# except:
|
| 2240 |
+
# pass
|
| 2241 |
+
|
| 2242 |
+
# # Calculate similarities for all reference images
|
| 2243 |
+
# embedding_results = []
|
| 2244 |
+
# phash_results = []
|
| 2245 |
+
# signature_results = []
|
| 2246 |
+
|
| 2247 |
+
# for j, ref_path in enumerate(paths_list):
|
| 2248 |
+
# # Embedding similarity
|
| 2249 |
+
# try:
|
| 2250 |
+
# ref_emb = ref_matrix[j]
|
| 2251 |
+
# emb_sim = float(np.dot(sprite_emb, ref_emb))
|
| 2252 |
+
# emb_sim = max(0.0, emb_sim) # Clamp negative values
|
| 2253 |
+
# except:
|
| 2254 |
+
# emb_sim = 0.0
|
| 2255 |
+
# embedding_results.append((ref_path, emb_sim))
|
| 2256 |
+
|
| 2257 |
+
# # Phash similarity
|
| 2258 |
+
# ph_sim = 0.0
|
| 2259 |
+
# if sprite_phash is not None and ref_path in hash_dict:
|
| 2260 |
+
# try:
|
| 2261 |
+
# ref_hash = hash_dict[ref_path]
|
| 2262 |
+
# hd = phash.hamming_distance(sprite_phash, ref_hash)
|
| 2263 |
+
# ph_sim = max(0.0, 1.0 - (hd / 64.0)) # Normalize to [0,1]
|
| 2264 |
+
# except:
|
| 2265 |
+
# pass
|
| 2266 |
+
# phash_results.append((ref_path, ph_sim))
|
| 2267 |
+
|
| 2268 |
+
# # Signature similarity
|
| 2269 |
+
# sig_sim = 0.0
|
| 2270 |
+
# if sprite_sig is not None and ref_path in signature_dict:
|
| 2271 |
+
# try:
|
| 2272 |
+
# ref_sig = signature_dict[ref_path]
|
| 2273 |
+
# dist = gis.normalized_distance(ref_sig, sprite_sig)
|
| 2274 |
+
# sig_sim = max(0.0, 1.0 - dist)
|
| 2275 |
+
# except:
|
| 2276 |
+
# pass
|
| 2277 |
+
# signature_results.append((ref_path, sig_sim))
|
| 2278 |
+
|
| 2279 |
+
# # Combine similarities using weighted approach
|
| 2280 |
+
# def normalize_scores(scores):
|
| 2281 |
+
# """Normalize scores to [0,1] range"""
|
| 2282 |
+
# if not scores:
|
| 2283 |
+
# return {}
|
| 2284 |
+
# vals = [s for _, s in scores if not math.isnan(s)]
|
| 2285 |
+
# if not vals:
|
| 2286 |
+
# return {p: 0.0 for p, _ in scores}
|
| 2287 |
+
# vmin, vmax = min(vals), max(vals)
|
| 2288 |
+
# if vmax == vmin:
|
| 2289 |
+
# return {p: 1.0 if s == vmax else 0.0 for p, s in scores}
|
| 2290 |
+
# return {p: (s - vmin) / (vmax - vmin) for p, s in scores}
|
| 2291 |
+
|
| 2292 |
+
# # Normalize each method's scores
|
| 2293 |
+
# emb_norm = normalize_scores(embedding_results)
|
| 2294 |
+
# ph_norm = normalize_scores(phash_results)
|
| 2295 |
+
# sig_norm = normalize_scores(signature_results)
|
| 2296 |
+
|
| 2297 |
+
# # Calculate weighted combined scores
|
| 2298 |
+
# w_emb, w_ph, w_sig = method_weights
|
| 2299 |
+
# combined_scores = []
|
| 2300 |
+
|
| 2301 |
+
# for ref_path in paths_list:
|
| 2302 |
+
# combined_score = (w_emb * emb_norm.get(ref_path, 0.0) +
|
| 2303 |
+
# w_ph * ph_norm.get(ref_path, 0.0) +
|
| 2304 |
+
# w_sig * sig_norm.get(ref_path, 0.0))
|
| 2305 |
+
# combined_scores.append((ref_path, combined_score))
|
| 2306 |
+
|
| 2307 |
+
# # Sort by combined score and apply thresholds
|
| 2308 |
+
# combined_scores.sort(key=lambda x: x[1], reverse=True)
|
| 2309 |
+
|
| 2310 |
+
# # Filter by minimum similarity if specified
|
| 2311 |
+
# if min_similarity is not None:
|
| 2312 |
+
# combined_scores = [(p, s) for p, s in combined_scores if s >= float(min_similarity)]
|
| 2313 |
+
|
| 2314 |
+
# # Get top-k matches
|
| 2315 |
+
# top_matches = combined_scores[:int(top_k)]
|
| 2316 |
+
|
| 2317 |
+
# # Convert to indices and scores
|
| 2318 |
+
# matched_indices = []
|
| 2319 |
+
# matched_scores = []
|
| 2320 |
+
|
| 2321 |
+
# for ref_path, score in top_matches:
|
| 2322 |
+
# try:
|
| 2323 |
+
# idx = paths_list.index(ref_path)
|
| 2324 |
+
# matched_indices.append(idx)
|
| 2325 |
+
# matched_scores.append(score)
|
| 2326 |
+
# except ValueError:
|
| 2327 |
+
# continue
|
| 2328 |
+
|
| 2329 |
+
# per_sprite_matched_indices.append(matched_indices)
|
| 2330 |
+
# per_sprite_scores.append(matched_scores)
|
| 2331 |
+
|
| 2332 |
+
# print(f"Sprite '{sprite_id}' matched {len(matched_indices)} references with scores: {matched_scores}")
|
| 2333 |
+
|
| 2334 |
+
# return per_sprite_matched_indices, per_sprite_scores, paths_list
|
| 2335 |
+
|
| 2336 |
+
# def choose_top_candidates_advanced(embedding_results, phash_results, imgmatch_results, top_k=10,
|
| 2337 |
+
# method_weights=(0.5, 0.3, 0.2), verbose=True):
|
| 2338 |
+
# """
|
| 2339 |
+
# Advanced candidate selection using multiple ranking methods
|
| 2340 |
+
|
| 2341 |
+
# Args:
|
| 2342 |
+
# embedding_results: list of (path, emb_sim)
|
| 2343 |
+
# phash_results: list of (path, hamming, ph_sim)
|
| 2344 |
+
# imgmatch_results: list of (path, dist, im_sim)
|
| 2345 |
+
# top_k: number of top candidates to return
|
| 2346 |
+
# method_weights: weights for (emb, phash, imgmatch)
|
| 2347 |
+
# verbose: whether to print detailed results
|
| 2348 |
+
|
| 2349 |
+
# Returns:
|
| 2350 |
+
# dict with top candidates from different methods and final selection
|
| 2351 |
+
# """
|
| 2352 |
+
# import math
|
| 2353 |
+
# from collections import defaultdict
|
| 2354 |
+
|
| 2355 |
+
# # Build dicts for quick lookup
|
| 2356 |
+
# emb_map = {p: float(s) for p, s in embedding_results}
|
| 2357 |
+
# ph_map = {p: float(sim) for p, _, sim in phash_results}
|
| 2358 |
+
# im_map = {p: float(sim) for p, _, sim in imgmatch_results}
|
| 2359 |
+
|
| 2360 |
+
# # Universe of candidates (union)
|
| 2361 |
+
# all_paths = sorted(set(list(emb_map.keys()) + list(ph_map.keys()) + list(im_map.keys())))
|
| 2362 |
+
|
| 2363 |
+
# # Normalize each metric across candidates to [0,1]
|
| 2364 |
+
# def normalize_map(m):
|
| 2365 |
+
# vals = [m.get(p, None) for p in all_paths]
|
| 2366 |
+
# present = [v for v in vals if v is not None and not math.isnan(v)]
|
| 2367 |
+
# if not present:
|
| 2368 |
+
# return {p: 0.0 for p in all_paths}
|
| 2369 |
+
# vmin, vmax = min(present), max(present)
|
| 2370 |
+
# if vmax == vmin:
|
| 2371 |
+
# return {p: (1.0 if (m.get(p, None) is not None) else 0.0) for p in all_paths}
|
| 2372 |
+
# norm = {}
|
| 2373 |
+
# for p in all_paths:
|
| 2374 |
+
# v = m.get(p, None)
|
| 2375 |
+
# if v is None or math.isnan(v):
|
| 2376 |
+
# norm[p] = 0.0
|
| 2377 |
+
# else:
|
| 2378 |
+
# norm[p] = max(0.0, min(1.0, (v - vmin) / (vmax - vmin)))
|
| 2379 |
+
# return norm
|
| 2380 |
+
|
| 2381 |
+
# # For embeddings, clamp negatives to 0 first
|
| 2382 |
+
# emb_map_clamped = {p: max(0.0, v) for p, v in emb_map.items()}
|
| 2383 |
+
|
| 2384 |
+
# emb_norm = normalize_map(emb_map_clamped)
|
| 2385 |
+
# ph_norm = normalize_map(ph_map)
|
| 2386 |
+
# im_norm = normalize_map(im_map)
|
| 2387 |
+
|
| 2388 |
+
# # Method A: Normalized weighted average
|
| 2389 |
+
# w_emb, w_ph, w_im = method_weights
|
| 2390 |
+
# weighted_scores = {}
|
| 2391 |
+
# for p in all_paths:
|
| 2392 |
+
# weighted_scores[p] = (w_emb * emb_norm.get(p, 0.0)
|
| 2393 |
+
# + w_ph * ph_norm.get(p, 0.0)
|
| 2394 |
+
# + w_im * im_norm.get(p, 0.0))
|
| 2395 |
+
|
| 2396 |
+
# top_weighted = sorted(weighted_scores.items(), key=lambda x: x[1], reverse=True)[:top_k]
|
| 2397 |
+
|
| 2398 |
+
# # Method B: Rank-sum (Borda)
|
| 2399 |
+
# def ranks_from_map(m_norm):
|
| 2400 |
+
# items = sorted(m_norm.items(), key=lambda x: x[1], reverse=True)
|
| 2401 |
+
# ranks = {}
|
| 2402 |
+
# for i, (p, _) in enumerate(items):
|
| 2403 |
+
# ranks[p] = i + 1 # 1-based
|
| 2404 |
+
# worst = len(items) + 1
|
| 2405 |
+
# for p in all_paths:
|
| 2406 |
+
# if p not in ranks:
|
| 2407 |
+
# ranks[p] = worst
|
| 2408 |
+
# return ranks
|
| 2409 |
+
|
| 2410 |
+
# rank_emb = ranks_from_map(emb_norm)
|
| 2411 |
+
# rank_ph = ranks_from_map(ph_norm)
|
| 2412 |
+
# rank_im = ranks_from_map(im_norm)
|
| 2413 |
+
|
| 2414 |
+
# rank_sum = {}
|
| 2415 |
+
# for p in all_paths:
|
| 2416 |
+
# rank_sum[p] = rank_emb.get(p, 9999) + rank_ph.get(p, 9999) + rank_im.get(p, 9999)
|
| 2417 |
+
# top_rank_sum = sorted(rank_sum.items(), key=lambda x: x[1])[:top_k] # smaller is better
|
| 2418 |
+
|
| 2419 |
+
# # Method C: Harmonic mean
|
| 2420 |
+
# harm_scores = {}
|
| 2421 |
+
# for p in all_paths:
|
| 2422 |
+
# a = emb_norm.get(p, 0.0)
|
| 2423 |
+
# b = ph_norm.get(p, 0.0)
|
| 2424 |
+
# c = im_norm.get(p, 0.0)
|
| 2425 |
+
# if a + b + c == 0 or a == 0 or b == 0 or c == 0:
|
| 2426 |
+
# harm = 0.0
|
| 2427 |
+
# else:
|
| 2428 |
+
# harm = 3.0 / ((1.0/a) + (1.0/b) + (1.0/c))
|
| 2429 |
+
# harm_scores[p] = harm
|
| 2430 |
+
# top_harm = sorted(harm_scores.items(), key=lambda x: x[1], reverse=True)[:top_k]
|
| 2431 |
+
|
| 2432 |
+
# # Consensus set: items in top-K of each metric
|
| 2433 |
+
# def topk_set_by_map(m_norm, k=top_k):
|
| 2434 |
+
# return set([p for p,_ in sorted(m_norm.items(), key=lambda x: x[1], reverse=True)[:k]])
|
| 2435 |
+
# cons_set = topk_set_by_map(emb_norm, top_k) & topk_set_by_map(ph_norm, top_k) & topk_set_by_map(im_norm, top_k)
|
| 2436 |
+
|
| 2437 |
+
# result = {
|
| 2438 |
+
# "emb_norm": emb_norm,
|
| 2439 |
+
# "ph_norm": ph_norm,
|
| 2440 |
+
# "im_norm": im_norm,
|
| 2441 |
+
# "weighted_topk": top_weighted,
|
| 2442 |
+
# "rank_sum_topk": top_rank_sum,
|
| 2443 |
+
# "harmonic_topk": top_harm,
|
| 2444 |
+
# "consensus_topk": list(cons_set),
|
| 2445 |
+
# "weighted_scores_full": weighted_scores,
|
| 2446 |
+
# "rank_sum_full": rank_sum,
|
| 2447 |
+
# "harmonic_full": harm_scores
|
| 2448 |
+
# }
|
| 2449 |
+
|
| 2450 |
+
# if verbose:
|
| 2451 |
+
# print(f"\nTop by Weighted Average (weights emb,ph,img = {w_emb:.2f},{w_ph:.2f},{w_im:.2f}):")
|
| 2452 |
+
# for i,(p,s) in enumerate(result["weighted_topk"], start=1):
|
| 2453 |
+
# print(f" {i}. {p} score={s:.4f} emb={emb_norm.get(p,0):.3f} ph={ph_norm.get(p,0):.3f} im={im_norm.get(p,0):.3f}")
|
| 2454 |
+
|
| 2455 |
+
# print("\nTop by Rank-sum (lower is better):")
|
| 2456 |
+
# for i,(p,s) in enumerate(result["rank_sum_topk"], start=1):
|
| 2457 |
+
# print(f" {i}. {p} rank_sum={s} emb_rank={rank_emb.get(p)} ph_rank={rank_ph.get(p)} img_rank={rank_im.get(p)}")
|
| 2458 |
+
|
| 2459 |
+
# print("\nTop by Harmonic mean:")
|
| 2460 |
+
# for i,(p,s) in enumerate(result["harmonic_topk"], start=1):
|
| 2461 |
+
# print(f" {i}. {p} harm={s:.4f} emb={emb_norm.get(p,0):.3f} ph={ph_norm.get(p,0):.3f} im={im_norm.get(p,0):.3f}")
|
| 2462 |
+
|
| 2463 |
+
# print(f"\nConsensus (in top-{top_k} of ALL metrics): {result['consensus_topk']}")
|
| 2464 |
+
|
| 2465 |
+
# # Final selection logic
|
| 2466 |
+
# final = None
|
| 2467 |
+
# if len(result["consensus_topk"]) > 0:
|
| 2468 |
+
# # Choose best-weighted among consensus
|
| 2469 |
+
# consensus = result["consensus_topk"]
|
| 2470 |
+
# best = max(consensus, key=lambda p: result["weighted_scores_full"].get(p, 0.0))
|
| 2471 |
+
# final = best
|
| 2472 |
+
# else:
|
| 2473 |
+
# final = result["weighted_topk"][0][0] if result["weighted_topk"] else None
|
| 2474 |
+
|
| 2475 |
+
# result["final_selection"] = final
|
| 2476 |
+
# return result
|
| 2477 |
+
|
| 2478 |
+
# # Use hybrid matching system
|
| 2479 |
+
# # BLOCKS_DIR = r"D:\DEV PATEL\2025\scratch_VLM\scratch_agent\blocks"
|
| 2480 |
+
# per_sprite_matched_indices, per_sprite_scores, paths_list = hybrid_similarity_matching(
|
| 2481 |
+
# sprite_images_bytes, sprite_ids, min_similarity, top_k, method_weights=(0.5, 0.3, 0.2)
|
| 2482 |
+
# )
|
| 2483 |
+
|
| 2484 |
+
# # =========================================
|
| 2485 |
+
# # Copy matched sprite assets + collect data
|
| 2486 |
+
# # =========================================
|
| 2487 |
+
# project_data = []
|
| 2488 |
+
# backdrop_data = []
|
| 2489 |
+
# copied_sprite_folders = set()
|
| 2490 |
+
# copied_backdrop_folders = set()
|
| 2491 |
+
|
| 2492 |
+
# # Flatten unique matched indices to process copying once per folder
|
| 2493 |
+
# matched_indices = sorted({idx for lst in per_sprite_matched_indices for idx in lst})
|
| 2494 |
+
# print("matched_indices------------------>",matched_indices)
|
| 2495 |
+
|
| 2496 |
+
# import shutil
|
| 2497 |
+
# import json
|
| 2498 |
+
# import os
|
| 2499 |
+
# from pathlib import Path
|
| 2500 |
+
|
| 2501 |
+
# # normalize base paths once before the loop
|
| 2502 |
+
# sprite_base_p = Path(sprite_base_path).resolve(strict=False)
|
| 2503 |
+
# backdrop_base_p = Path(backdrop_base_path).resolve(strict=False)
|
| 2504 |
+
# project_folder_p = Path(project_folder)
|
| 2505 |
+
# project_folder_p.mkdir(parents=True, exist_ok=True)
|
| 2506 |
+
|
| 2507 |
+
# copied_sprite_folders = set()
|
| 2508 |
+
# copied_backdrop_folders = set()
|
| 2509 |
+
|
| 2510 |
+
# def display_like_windows_no_lead(p: Path) -> str:
|
| 2511 |
+
# """
|
| 2512 |
+
# For human-readable logs only β convert Path to a string like:
|
| 2513 |
+
# "app\\blocks\\Backdrops\\Castle 2.sb3" (no leading slash).
|
| 2514 |
+
# """
|
| 2515 |
+
# s = p.as_posix() # forward-slash string, safe for Path objects
|
| 2516 |
+
# if s.startswith("/"):
|
| 2517 |
+
# s = s[1:]
|
| 2518 |
+
# return s.replace("/", "\\")
|
| 2519 |
+
|
| 2520 |
+
# def is_subpath(child: Path, parent: Path) -> bool:
|
| 2521 |
+
# """Robust membership test: is child under parent?"""
|
| 2522 |
+
# try:
|
| 2523 |
+
# # use non-strict resolve only if needed, but avoid exceptions
|
| 2524 |
+
# child.relative_to(parent)
|
| 2525 |
+
# return True
|
| 2526 |
+
# except Exception:
|
| 2527 |
+
# return False
|
| 2528 |
+
|
| 2529 |
+
# # Flatten unique matched indices (if not already)
|
| 2530 |
+
# matched_indices = sorted({idx for lst in per_sprite_matched_indices for idx in lst})
|
| 2531 |
+
# print("matched_indices------------------>", matched_indices)
|
| 2532 |
+
|
| 2533 |
+
# for matched_idx in matched_indices:
|
| 2534 |
+
# # defensive check
|
| 2535 |
+
# if not (0 <= matched_idx < len(paths_list)):
|
| 2536 |
+
# print(f" β matched_idx {matched_idx} out of range, skipping")
|
| 2537 |
+
# continue
|
| 2538 |
+
|
| 2539 |
+
# matched_image_path = paths_list[matched_idx]
|
| 2540 |
+
# matched_path_p = Path(matched_image_path).resolve(strict=False) # keep as Path
|
| 2541 |
+
# matched_folder_p = matched_path_p.parent # Path object
|
| 2542 |
+
# matched_filename = matched_path_p.name
|
| 2543 |
+
|
| 2544 |
+
# # Prepare display-only string (do NOT reassign matched_folder_p)
|
| 2545 |
+
# matched_folder_display = display_like_windows_no_lead(matched_folder_p)
|
| 2546 |
+
|
| 2547 |
+
# print(f"Processing matched image: {matched_image_path}")
|
| 2548 |
+
# print(f" - Folder: {matched_folder_display}")
|
| 2549 |
+
# print(f" - Sprite path: {display_like_windows_no_lead(sprite_base_p)}")
|
| 2550 |
+
# print(f" - Backdrop path: {display_like_windows_no_lead(backdrop_base_p)}")
|
| 2551 |
+
# print(f" - Filename: {matched_filename}")
|
| 2552 |
+
|
| 2553 |
+
# # Use a canonical string to store in the copied set (POSIX absolute-ish)
|
| 2554 |
+
# folder_key = matched_folder_p.as_posix()
|
| 2555 |
+
|
| 2556 |
+
# # ---------- SPRITE ----------
|
| 2557 |
+
# if is_subpath(matched_folder_p, sprite_base_p) and folder_key not in copied_sprite_folders:
|
| 2558 |
+
# print(f"Processing SPRITE folder: {matched_folder_display}")
|
| 2559 |
+
# copied_sprite_folders.add(folder_key)
|
| 2560 |
+
|
| 2561 |
+
# sprite_json_path = matched_folder_p / "sprite.json"
|
| 2562 |
+
# print("sprite_json_path----------------------->", sprite_json_path)
|
| 2563 |
+
# print("copied sprite folder----------------------->", copied_sprite_folders)
|
| 2564 |
+
# if sprite_json_path.exists() and sprite_json_path.is_file():
|
| 2565 |
+
# try:
|
| 2566 |
+
# with sprite_json_path.open("r", encoding="utf-8") as f:
|
| 2567 |
+
# sprite_info = json.load(f)
|
| 2568 |
+
# project_data.append(sprite_info)
|
| 2569 |
+
# print(f" β Successfully read sprite.json from {matched_folder_display}")
|
| 2570 |
+
# except Exception as e:
|
| 2571 |
+
# print(f" β Failed to read sprite.json in {matched_folder_display}: {repr(e)}")
|
| 2572 |
+
# else:
|
| 2573 |
+
# print(f" β No sprite.json in {matched_folder_display}")
|
| 2574 |
+
|
| 2575 |
+
# # copy non-matching files from the sprite folder (except matched image and sprite.json)
|
| 2576 |
+
# try:
|
| 2577 |
+
# sprite_files = list(matched_folder_p.iterdir())
|
| 2578 |
+
# except Exception as e:
|
| 2579 |
+
# sprite_files = []
|
| 2580 |
+
# print(f" β Failed to list files in {matched_folder_display}: {repr(e)}")
|
| 2581 |
+
|
| 2582 |
+
# print(f" Files in sprite folder: {[p.name for p in sprite_files]}")
|
| 2583 |
+
# for p in sprite_files:
|
| 2584 |
+
# fname = p.name
|
| 2585 |
+
# if fname in (matched_filename, "sprite.json"):
|
| 2586 |
+
# print(f" Skipping {fname} (matched image or sprite.json)")
|
| 2587 |
+
# continue
|
| 2588 |
+
# if p.is_file():
|
| 2589 |
+
# dst = project_folder_p / fname
|
| 2590 |
+
# try:
|
| 2591 |
+
# shutil.copy2(str(p), str(dst))
|
| 2592 |
+
# print(f" β Copied sprite asset: {p} -> {dst}")
|
| 2593 |
+
# except Exception as e:
|
| 2594 |
+
# print(f" β Failed to copy sprite asset {p}: {repr(e)}")
|
| 2595 |
+
# else:
|
| 2596 |
+
# print(f" Skipping {fname} (not a file)")
|
| 2597 |
+
|
| 2598 |
+
# # ---------- BACKDROP ----------
|
| 2599 |
+
# if is_subpath(matched_folder_p, backdrop_base_p) and folder_key not in copied_backdrop_folders:
|
| 2600 |
+
# print(f"Processing BACKDROP folder: {matched_folder_display}")
|
| 2601 |
+
# copied_backdrop_folders.add(folder_key)
|
| 2602 |
+
# print("backdrop_base_path----------------------->", display_like_windows_no_lead(backdrop_base_p))
|
| 2603 |
+
# print("copied backdrop folder----------------------->", copied_backdrop_folders)
|
| 2604 |
+
|
| 2605 |
+
# # copy matched backdrop image
|
| 2606 |
+
# backdrop_src = matched_folder_p / matched_filename
|
| 2607 |
+
# backdrop_dst = project_folder_p / matched_filename
|
| 2608 |
+
# if backdrop_src.exists() and backdrop_src.is_file():
|
| 2609 |
+
# try:
|
| 2610 |
+
# shutil.copy2(str(backdrop_src), str(backdrop_dst))
|
| 2611 |
+
# print(f" β Copied matched backdrop image: {backdrop_src} -> {backdrop_dst}")
|
| 2612 |
+
# except Exception as e:
|
| 2613 |
+
# print(f" β Failed to copy matched backdrop image {backdrop_src}: {repr(e)}")
|
| 2614 |
+
# else:
|
| 2615 |
+
# print(f" β Matched backdrop source not found: {backdrop_src}")
|
| 2616 |
+
|
| 2617 |
+
# # copy other files from folder (skip project.json and matched image)
|
| 2618 |
+
# try:
|
| 2619 |
+
# backdrop_files = list(matched_folder_p.iterdir())
|
| 2620 |
+
# except Exception as e:
|
| 2621 |
+
# backdrop_files = []
|
| 2622 |
+
# print(f" β Failed to list files in {matched_folder_display}: {repr(e)}")
|
| 2623 |
+
|
| 2624 |
+
# print(f" Files in backdrop folder: {[p.name for p in backdrop_files]}")
|
| 2625 |
+
# for p in backdrop_files:
|
| 2626 |
+
# fname = p.name
|
| 2627 |
+
# if fname in (matched_filename, "project.json"):
|
| 2628 |
+
# print(f" Skipping {fname} (matched image or project.json)")
|
| 2629 |
+
# continue
|
| 2630 |
+
# if p.is_file():
|
| 2631 |
+
# dst = project_folder_p / fname
|
| 2632 |
+
# try:
|
| 2633 |
+
# shutil.copy2(str(p), str(dst))
|
| 2634 |
+
# print(f" β Copied backdrop asset: {p} -> {dst}")
|
| 2635 |
+
# except Exception as e:
|
| 2636 |
+
# print(f" β Failed to copy backdrop asset {p}: {repr(e)}")
|
| 2637 |
+
# else:
|
| 2638 |
+
# print(f" Skipping {fname} (not a file)")
|
| 2639 |
+
|
| 2640 |
+
# # read project.json to extract Stage/targets
|
| 2641 |
+
# pj = matched_folder_p / "project.json"
|
| 2642 |
+
# if pj.exists() and pj.is_file():
|
| 2643 |
+
# try:
|
| 2644 |
+
# with pj.open("r", encoding="utf-8") as f:
|
| 2645 |
+
# bd_json = json.load(f)
|
| 2646 |
+
# stage_count = 0
|
| 2647 |
+
# for tgt in bd_json.get("targets", []):
|
| 2648 |
+
# if tgt.get("isStage"):
|
| 2649 |
+
# backdrop_data.append(tgt)
|
| 2650 |
+
# stage_count += 1
|
| 2651 |
+
# print(f" β Successfully read project.json from {matched_folder_display}, found {stage_count} stage(s)")
|
| 2652 |
+
# except Exception as e:
|
| 2653 |
+
# print(f" β Failed to read project.json in {matched_folder_display}: {repr(e)}")
|
| 2654 |
+
# else:
|
| 2655 |
+
# print(f" β No project.json in {matched_folder_display}")
|
| 2656 |
+
|
| 2657 |
+
# print("---")
|
| 2658 |
+
|
| 2659 |
+
# final_project = {
|
| 2660 |
+
# "targets": [], "monitors": [], "extensions": [],
|
| 2661 |
+
# "meta": {
|
| 2662 |
+
# "semver": "3.0.0",
|
| 2663 |
+
# "vm": "11.3.0",
|
| 2664 |
+
# "agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
|
| 2665 |
+
# }
|
| 2666 |
+
# }
|
| 2667 |
+
|
| 2668 |
+
|
| 2669 |
+
# # Add sprite targets (non-stage)
|
| 2670 |
+
# for spr in project_data:
|
| 2671 |
+
# if not spr.get("isStage", False):
|
| 2672 |
+
# final_project["targets"].append(spr)
|
| 2673 |
+
|
| 2674 |
+
# # then backdrop as the Stage
|
| 2675 |
+
# if backdrop_data:
|
| 2676 |
+
# all_costumes, sounds = [], []
|
| 2677 |
+
# seen_costumes = set()
|
| 2678 |
+
# for i, bd in enumerate(backdrop_data):
|
| 2679 |
+
# for costume in bd.get("costumes", []):
|
| 2680 |
+
# # Create a unique key for the costume
|
| 2681 |
+
# key = (costume.get("name"), costume.get("assetId"))
|
| 2682 |
+
# if key not in seen_costumes:
|
| 2683 |
+
# seen_costumes.add(key)
|
| 2684 |
+
# all_costumes.append(costume)
|
| 2685 |
+
|
| 2686 |
+
# if i == 0:
|
| 2687 |
+
# sounds = bd.get("sounds", [])
|
| 2688 |
+
# stage_obj={
|
| 2689 |
+
# "isStage": True,
|
| 2690 |
+
# "name": "Stage",
|
| 2691 |
+
# "objName": "Stage",
|
| 2692 |
+
# "variables": {},
|
| 2693 |
+
# "lists": {},
|
| 2694 |
+
# "broadcasts": {},
|
| 2695 |
+
# "blocks": {},
|
| 2696 |
+
# "comments": {},
|
| 2697 |
+
# "currentCostume": 1 if len(all_costumes) > 1 else 0,
|
| 2698 |
+
# "costumes": all_costumes,
|
| 2699 |
+
# "sounds": sounds,
|
| 2700 |
+
# "volume": 100,
|
| 2701 |
+
# "layerOrder": 0,
|
| 2702 |
+
# "tempo": 60,
|
| 2703 |
+
# "videoTransparency": 50,
|
| 2704 |
+
# "videoState": "on",
|
| 2705 |
+
# "textToSpeechLanguage": None
|
| 2706 |
+
# }
|
| 2707 |
+
# final_project["targets"].insert(0, stage_obj)
|
| 2708 |
+
# else:
|
| 2709 |
+
# logger.warning("β οΈ No backdrop matched. Using default static backdrop.")
|
| 2710 |
+
# default_backdrop_path = BACKDROP_DIR / "cd21514d0531fdffb22204e0ec5ed84a.svg"
|
| 2711 |
+
# default_backdrop_name = "cd21514d0531fdffb22204e0ec5ed84a.svg"
|
| 2712 |
+
|
| 2713 |
+
# default_backdrop_sound = BACKDROP_DIR / "83a9787d4cb6f3b7632b4ddfebf74367.wav"
|
| 2714 |
+
# default_backdrop_sound_name = "cd21514d0531fdffb22204e0ec5ed84a.svg"
|
| 2715 |
+
# try:
|
| 2716 |
+
# shutil.copy2(default_backdrop_path, os.path.join(project_folder, default_backdrop_name))
|
| 2717 |
+
# logger.info(f"β
Default backdrop copied to project: {default_backdrop_name}")
|
| 2718 |
+
|
| 2719 |
+
# shutil.copy2(default_backdrop_sound, os.path.join(project_folder, default_backdrop_sound_name))
|
| 2720 |
+
# logger.info(f"β
Default backdrop sound copied to project: {default_backdrop_sound_name}")
|
| 2721 |
+
# except Exception as e:
|
| 2722 |
+
# logger.error(f"β Failed to copy default backdrop: {e}")
|
| 2723 |
+
|
| 2724 |
+
# stage_obj={
|
| 2725 |
+
# "isStage": True,
|
| 2726 |
+
# "name": "Stage",
|
| 2727 |
+
# "objName": "Stage",
|
| 2728 |
+
# "variables": {},
|
| 2729 |
+
# "lists": {},
|
| 2730 |
+
# "broadcasts": {},
|
| 2731 |
+
# "blocks": {},
|
| 2732 |
+
# "comments": {},
|
| 2733 |
+
# "currentCostume": 0,
|
| 2734 |
+
# "costumes": [
|
| 2735 |
+
# {
|
| 2736 |
+
# "assetId": default_backdrop_name.split(".")[0],
|
| 2737 |
+
# "name": "defaultBackdrop",
|
| 2738 |
+
# "md5ext": default_backdrop_name,
|
| 2739 |
+
# "dataFormat": "svg",
|
| 2740 |
+
# "rotationCenterX": 240,
|
| 2741 |
+
# "rotationCenterY": 180
|
| 2742 |
+
# }
|
| 2743 |
+
# ],
|
| 2744 |
+
# "sounds": [
|
| 2745 |
+
# {
|
| 2746 |
+
# "name": "pop",
|
| 2747 |
+
# "assetId": "83a9787d4cb6f3b7632b4ddfebf74367",
|
| 2748 |
+
# "dataFormat": "wav",
|
| 2749 |
+
# "format": "",
|
| 2750 |
+
# "rate": 48000,
|
| 2751 |
+
# "sampleCount": 1123,
|
| 2752 |
+
# "md5ext": "83a9787d4cb6f3b7632b4ddfebf74367.wav"
|
| 2753 |
+
# }
|
| 2754 |
+
# ],
|
| 2755 |
+
# "volume": 100,
|
| 2756 |
+
# "layerOrder": 0,
|
| 2757 |
+
# "tempo": 60,
|
| 2758 |
+
# "videoTransparency": 50,
|
| 2759 |
+
# "videoState": "on",
|
| 2760 |
+
# "textToSpeechLanguage": None
|
| 2761 |
+
# }
|
| 2762 |
+
# final_project["targets"].insert(0, stage_obj)
|
| 2763 |
+
|
| 2764 |
+
# with open(project_json_path, 'w') as f:
|
| 2765 |
+
# json.dump(final_project, f, indent=2)
|
| 2766 |
+
|
| 2767 |
+
# return project_json_path
|
| 2768 |
|
| 2769 |
|
| 2770 |
def convert_pdf_stream_to_images(pdf_stream: io.BytesIO, dpi=300):
|