prthm11 commited on
Commit
1ada1b9
Β·
verified Β·
1 Parent(s): 21c4197

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1099 -449
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, method_weights=(0.5, 0.3, 0.2), verbose=True):
 
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
- ''' Extract images from PDF and generate structured sprite JSON '''
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1388
  manipulated_json = {}
1389
- img_elements = []
1390
  try:
1391
-
1392
- if isinstance(pdf_stream, io.BytesIO):
1393
- # use a random ID since there's no filename
1394
- pdf_id = uuid.uuid4().hex
1395
- else:
1396
- pdf_id = os.path.splitext(os.path.basename(pdf_stream))[0]
1397
-
1398
- try:
1399
- elements = partition_pdf(
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
- img_b64 = el["metadata"].get("image_base64")
1418
- if not img_b64:
 
 
1419
  continue
1420
-
 
 
 
 
1421
  manipulated_json[f"Sprite {sprite_count}"] = {
1422
- "base64": el["metadata"]["image_base64"],
1423
- "file-path": pdf_id,
 
 
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
- ''' It appends all the list and paths from json files and pick the best match's path'''
 
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 = os.path.normpath(str(BACKDROP_DIR))
1438
- sprite_base_path = os.path.normpath(str(SPRITE_DIR))
1439
- code_blocks_path = os.path.normpath(str(CODE_BLOCKS_DIR))
 
 
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
- img = Image.open(BytesIO(base64.b64decode(b64.split(",")[-1]))).convert("RGB")
 
 
 
 
 
1454
  buffer = BytesIO()
1455
  img.save(buffer, format="PNG")
1456
  buffer.seek(0)
1457
  sprite_images_bytes.append(buffer)
1458
-
1459
- # -----------------------------------------
1460
- # Hybrid Similarity Matching System
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
- with open(embeddings_path, "r", encoding="utf-8") as f:
1490
- embedding_json = json.load(f)
1491
-
1492
- # Load phash data (if exists)
 
 
1493
  hash_dict = {}
1494
  if os.path.exists(hash_path):
1495
- with open(hash_path, "r", encoding="utf-8") as f:
1496
- hash_data = json.load(f)
1497
- for path, hash_str in hash_data.items():
1498
- try:
1499
- hash_dict[path] = phash.hex_to_hash(hash_str)
1500
- except:
1501
- pass
1502
-
1503
- # Load signature data (if exists)
 
 
 
1504
  signature_dict = {}
1505
- gis = ImageSignature()
1506
  if os.path.exists(signature_path):
1507
- with open(signature_path, "r", encoding="utf-8") as f:
1508
- sig_data = json.load(f)
1509
- for path, sig_list in sig_data.items():
1510
- try:
1511
- signature_dict[path] = np.array(sig_list)
1512
- except:
1513
- pass
1514
-
1515
- # Parse embeddings
 
 
 
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
- raise RuntimeError("No reference images/embeddings found")
1545
-
 
 
1546
  ref_matrix = np.vstack(embeddings_list).astype(np.float32)
1547
 
1548
- # Process input sprites
1549
- # init_dinov2()
1550
- per_sprite_matched_indices = []
1551
- per_sprite_scores = []
1552
-
1553
- for i, (sprite_bytes, sprite_id) in enumerate(zip(sprite_images_bytes, sprite_ids)):
1554
- print(f"Processing sprite {i+1}/{len(sprite_ids)}: {sprite_id}")
1555
-
1556
- # Convert bytes to PIL for processing
1557
  sprite_pil = Image.open(sprite_bytes)
1558
- if sprite_pil is None:
1559
- per_sprite_matched_indices.append([])
1560
- per_sprite_scores.append([])
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
- sprite_emb = np.zeros(ref_matrix.shape[1])
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
- sprite_phash = phash.encode_image(image_array=sprite_hash_arr)
1579
- except:
1580
- pass
1581
-
1582
- # 3. Compute image signature
1583
  sprite_sig = None
1584
- try:
1585
- temp_path = f"temp_sprite_{i}.png"
1586
- enhanced_sprite.save(temp_path, format="PNG")
1587
- sprite_sig = gis.generate_signature(temp_path)
1588
- os.remove(temp_path)
1589
- except:
1590
- pass
1591
-
1592
- # Calculate similarities for all reference images
1593
- embedding_results = []
1594
- phash_results = []
1595
- signature_results = []
1596
-
1597
- for j, ref_path in enumerate(paths_list):
1598
- # Embedding similarity
1599
- try:
1600
- ref_emb = ref_matrix[j]
1601
- emb_sim = float(np.dot(sprite_emb, ref_emb))
1602
- emb_sim = max(0.0, emb_sim) # Clamp negative values
1603
- except:
1604
- emb_sim = 0.0
1605
- embedding_results.append((ref_path, emb_sim))
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
- harm = 3.0 / ((1.0/a) + (1.0/b) + (1.0/c))
1779
- harm_scores[p] = harm
1780
- top_harm = sorted(harm_scores.items(), key=lambda x: x[1], reverse=True)[:top_k]
1781
-
1782
- # Consensus set: items in top-K of each metric
1783
- def topk_set_by_map(m_norm, k=top_k):
1784
- return set([p for p,_ in sorted(m_norm.items(), key=lambda x: x[1], reverse=True)[:k]])
1785
- 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)
1786
-
1787
- result = {
1788
- "emb_norm": emb_norm,
1789
- "ph_norm": ph_norm,
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
- # Final selection logic
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
- # Flatten unique matched indices (if not already)
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) # keep as Path
1891
- matched_folder_p = matched_path_p.parent # Path object
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
- print(f" - Sprite path: {display_like_windows_no_lead(sprite_base_p)}")
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
- # ---------- SPRITE ----------
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
- else:
1946
- print(f" Skipping {fname} (not a file)")
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
- else:
2005
- print(f" ⚠ No project.json in {matched_folder_display}")
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):