Update app.py
Browse files
app.py
CHANGED
|
@@ -22,10 +22,11 @@ from langchain_core.utils.utils import secret_from_env
|
|
| 22 |
from io import BytesIO
|
| 23 |
from pathlib import Path
|
| 24 |
import os
|
| 25 |
-
from utils.block_relation_builder import block_builder, separate_scripts, transform_logic_to_action_flow, analyze_opcode_counts
|
| 26 |
from langchain.chat_models import ChatOpenAI
|
| 27 |
from langchain_openai import ChatOpenAI
|
| 28 |
from pydantic import Field, SecretStr
|
|
|
|
| 29 |
|
| 30 |
os.environ["OPENROUTER_API_KEY"] = os.getenv("OPENROUTER_API_KEY", "default_key_or_placeholder")
|
| 31 |
class ChatOpenRouter(ChatOpenAI):
|
|
@@ -165,76 +166,118 @@ class GameState(TypedDict):
|
|
| 165 |
|
| 166 |
# Refined SYSTEM_PROMPT with more explicit Scratch JSON rules, especially for variables
|
| 167 |
SYSTEM_PROMPT = """
|
| 168 |
-
You are an expert
|
| 169 |
-
Your
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
* Examples:
|
| 208 |
-
* Number: `"STEPS": [1, [4, "10"]]` (for `move 10 steps` block).
|
| 209 |
-
* String/Text: `"MESSAGE": [1, [7, "Hello"]]` (for `say Hello` block).
|
| 210 |
-
* String/Message (common for text inputs): `"MESSAGE": [1, [10, "Hello!"]]` (for `say Hello! for 2 secs`).
|
| 211 |
-
* **C-Block Substack (blocks within a loop or conditional):**
|
| 212 |
-
* Format: `"<SUBSTACK_NAME>": [2, "<blockId_of_first_block_in_substack>"]`
|
| 213 |
-
* Common `SUBSTACK_NAME` values are `SUBSTACK` (for `if`, `forever`, `repeat`) and `SUBSTACK2` (for `else` in `if else`).
|
| 214 |
-
* Example: `"SUBSTACK": [2, "firstBlockInLoopId"]`
|
| 215 |
-
|
| 216 |
-
4. **`fields` Property Details (for dropdowns or direct internal values):**
|
| 217 |
-
* Used for dropdown menus, variable names, list names, or other static selections directly within the block.
|
| 218 |
-
* Format: `"<FIELD_NAME>": ["<selected_value>", null]`
|
| 219 |
-
* Examples:
|
| 220 |
-
* Dropdown: `"KEY_OPTION": ["space", null]` (for `when space key pressed`).
|
| 221 |
-
* Variable Name: `"VARIABLE": ["score", null]` (for `set score to 0`).
|
| 222 |
-
* Direction (specific motion block): `"FORWARD_BACKWARD": ["forward", null]` (for `go forward layers`).
|
| 223 |
-
|
| 224 |
-
5. **Unique IDs:**
|
| 225 |
-
* All block IDs, variable IDs, and list IDs must be unique strings (e.g., "myBlock123", "myVarId456", "myListId789"). Do NOT use placeholder strings like "block_id_here".
|
| 226 |
-
|
| 227 |
-
6. **No Nested `blocks` Dictionary:**
|
| 228 |
-
* The `blocks` dictionary should only appear once per `target` (sprite/stage). Do NOT nest a `blocks` dictionary inside an individual block definition. Blocks that are part of a substack are linked via the `SUBSTACK` input.
|
| 229 |
-
|
| 230 |
-
7. **Asset Properties (for Costumes/Sounds):**
|
| 231 |
-
* `assetId`, `md5ext`, `bitmapResolution`, `rotationCenterX`/`rotationCenterY` should be correctly associated with costume and sound objects within the `costumes` and `sounds` arrays.
|
| 232 |
-
|
| 233 |
-
**General Principles and Important Considerations:**
|
| 234 |
-
* **Backward Compatibility:** Adhere strictly to existing Scratch 3.0 opcodes and schema to ensure backward compatibility with older projects. [cite_start]Opcodes must remain consistent to prevent previously saved projects from failing to load or behaving unexpectedly[cite: 18, 19, 25, 65].
|
| 235 |
-
* **Forgiving Inputs:** Recognize that Scratch is designed to be "forgiving in its interpretation of inputs." [cite_start]The Scratch VM handles potentially "invalid" inputs gracefully (e.g., converting a number to a string if expected, returning default values like zero or empty strings, or performing no action) rather than crashing[cite: 20, 21, 22, 38, 39, 41]. This implies that precise type matching for inputs might be handled internally by Scratch, allowing for some flexibility in how values are provided, but the agent should aim for the most common and logical type.
|
| 236 |
"""
|
| 237 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
SYSTEM_PROMPT_JSON_CORRECTOR ="""
|
| 239 |
You are an assistant that outputs JSON responses strictly following the given schema.
|
| 240 |
If the JSON you produce has any formatting errors, missing required fields, or invalid structure, you must identify the problems and correct them.
|
|
@@ -254,7 +297,6 @@ agent = create_react_agent(
|
|
| 254 |
tools=[], # No specific tools are defined here, but could be added later
|
| 255 |
prompt=SYSTEM_PROMPT
|
| 256 |
)
|
| 257 |
-
|
| 258 |
agent_2 = create_react_agent(
|
| 259 |
model=llm2,
|
| 260 |
tools=[], # No specific tools are defined here, but could be added later
|
|
@@ -503,56 +545,62 @@ hat_description = hat_block_data["description"]
|
|
| 503 |
#hat_description = hat_block_data.get("description", "No description available")
|
| 504 |
# hat_opcodes_functionalities = "\n".join([f" - Opcode: {block['op_code']}, functionality: {block['functionality']} example: standalone use: {block['example_standalone']}" for block in hat_block_data["blocks"]])
|
| 505 |
hat_opcodes_functionalities = "\n".join([
|
| 506 |
-
f" - Opcode: {block.get('op_code', 'N/A')}, functionality: {block.get('functionality', 'N/A')}, example: standalone use {block.get('example_standalone', 'N/A')}"
|
|
|
|
| 507 |
for block in hat_block_data.get("blocks", [])
|
| 508 |
]) if isinstance(hat_block_data.get("blocks"), list) else " No blocks information available."
|
| 509 |
-
hat_opcodes_functionalities = os.path.join(BLOCKS_DIR, "hat_blocks.txt")
|
| 510 |
print("Hat blocks loaded successfully.", hat_description)
|
| 511 |
|
| 512 |
boolean_block_data = _load_block_catalog(BOOLEAN_BLOCKS_PATH)
|
| 513 |
boolean_description = boolean_block_data["description"]
|
| 514 |
# boolean_opcodes_functionalities = "\n".join([f" - Opcode: {block['op_code']}, functionality: {block['functionality']} example: standalone use: {block['example_standalone']}" for block in boolean_block_data["blocks"]])
|
| 515 |
boolean_opcodes_functionalities = "\n".join([
|
| 516 |
-
f" - Opcode: {block.get('op_code', 'N/A')}, functionality: {block.get('functionality', 'N/A')}, example: standalone use {block.get('example_standalone', 'N/A')}"
|
|
|
|
| 517 |
for block in boolean_block_data.get("blocks", [])
|
| 518 |
]) if isinstance(boolean_block_data.get("blocks"), list) else " No blocks information available."
|
| 519 |
-
boolean_opcodes_functionalities = os.path.join(BLOCKS_DIR, "boolean_blocks.txt")
|
| 520 |
|
| 521 |
c_block_data = _load_block_catalog(C_BLOCKS_PATH)
|
| 522 |
c_description = c_block_data["description"]
|
| 523 |
# c_opcodes_functionalities = "\n".join([f" - Opcode: {block['op_code']}, functionality: {block['functionality']} example: standalone use: {block['example_standalone']}" for block in c_block_data["blocks"]])
|
| 524 |
c_opcodes_functionalities = "\n".join([
|
| 525 |
-
f" - Opcode: {block.get('op_code', 'N/A')}, functionality: {block.get('functionality', 'N/A')}, example: standalone use {block.get('example_standalone', 'N/A')}"
|
|
|
|
| 526 |
for block in c_block_data.get("blocks", [])
|
| 527 |
]) if isinstance(c_block_data.get("blocks"), list) else " No blocks information available."
|
| 528 |
-
c_opcodes_functionalities = os.path.join(BLOCKS_DIR, "c_blocks.txt")
|
| 529 |
|
| 530 |
cap_block_data = _load_block_catalog(CAP_BLOCKS_PATH)
|
| 531 |
cap_description = cap_block_data["description"]
|
| 532 |
# cap_opcodes_functionalities = "\n".join([f" - Opcode: {block['op_code']}, functionality: {block['functionality']} example: standalone use: {block['example_standalone']}" for block in cap_block_data["blocks"]])
|
| 533 |
cap_opcodes_functionalities = "\n".join([
|
| 534 |
-
f" - Opcode: {block.get('op_code', 'N/A')}, functionality: {block.get('functionality', 'N/A')}, example: standalone use {block.get('example_standalone', 'N/A')}"
|
|
|
|
| 535 |
for block in cap_block_data.get("blocks", [])
|
| 536 |
]) if isinstance(cap_block_data.get("blocks"), list) else " No blocks information available."
|
| 537 |
-
cap_opcodes_functionalities = os.path.join(BLOCKS_DIR, "cap_blocks.txt")
|
| 538 |
|
| 539 |
reporter_block_data = _load_block_catalog(REPORTER_BLOCKS_PATH)
|
| 540 |
reporter_description = reporter_block_data["description"]
|
| 541 |
# reporter_opcodes_functionalities = "\n".join([f" - Opcode: {block['op_code']}, functionality: {block['functionality']} example: standalone use: {block['example_standalone']}" for block in reporter_block_data["blocks"]])
|
| 542 |
reporter_opcodes_functionalities = "\n".join([
|
| 543 |
-
f" - Opcode: {block.get('op_code', 'N/A')}, functionality: {block.get('functionality', 'N/A')}, example: standalone use {block.get('example_standalone', 'N/A')}"
|
|
|
|
| 544 |
for block in reporter_block_data.get("blocks", [])
|
| 545 |
]) if isinstance(reporter_block_data.get("blocks"), list) else " No blocks information available."
|
| 546 |
-
reporter_opcodes_functionalities = os.path.join(BLOCKS_DIR, "reporter_blocks.txt")
|
| 547 |
|
| 548 |
stack_block_data = _load_block_catalog(STACK_BLOCKS_PATH)
|
| 549 |
stack_description = stack_block_data["description"]
|
| 550 |
# stack_opcodes_functionalities = "\n".join([f" - Opcode: {block['op_code']}, functionality: {block['functionality']} example: standalone use: {block['example_standalone']}" for block in stack_block_data["blocks"]])
|
| 551 |
stack_opcodes_functionalities = "\n".join([
|
| 552 |
-
f" - Opcode: {block.get('op_code', 'N/A')}, functionality: {block.get('functionality', 'N/A')}, example: standalone use {block.get('example_standalone', 'N/A')}"
|
|
|
|
| 553 |
for block in stack_block_data.get("blocks", [])
|
| 554 |
]) if isinstance(stack_block_data.get("blocks"), list) else " No blocks information available."
|
| 555 |
-
stack_opcodes_functionalities = os.path.join(BLOCKS_DIR, "stack_blocks.txt")
|
| 556 |
|
| 557 |
# This makes ALL_SCRATCH_BLOCKS_CATALOG available globally
|
| 558 |
ALL_SCRATCH_BLOCKS_CATALOG = _load_block_catalog(BLOCK_CATALOG_PATH)
|
|
@@ -623,123 +671,269 @@ def extract_json_from_llm_response(raw_response: str) -> dict:
|
|
| 623 |
logger.error("Sanitized JSON still invalid:\n%s", json_string)
|
| 624 |
raise
|
| 625 |
|
| 626 |
-
# def
|
| 627 |
# """
|
| 628 |
-
#
|
| 629 |
-
|
| 630 |
-
# Accepts:
|
| 631 |
-
# - a list of base64 strings → picks the first element
|
| 632 |
-
# - a PIL Image instance → encodes to PNG/base64
|
| 633 |
-
# - a raw base64 string → strips whitespace and data URI prefix
|
| 634 |
# """
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 635 |
# if not raw_b64:
|
| 636 |
-
# return ""
|
| 637 |
|
| 638 |
-
# # 1. If it’s a list, take its first element
|
| 639 |
# if isinstance(raw_b64, list):
|
| 640 |
# raw_b64 = raw_b64[0] if raw_b64 else ""
|
| 641 |
# if not raw_b64:
|
| 642 |
-
# return ""
|
| 643 |
|
| 644 |
-
# # 2. If it’s a PIL Image, convert to base64
|
| 645 |
# if isinstance(raw_b64, Image.Image):
|
| 646 |
# buf = io.BytesIO()
|
| 647 |
# raw_b64.save(buf, format="PNG")
|
| 648 |
# raw_b64 = base64.b64encode(buf.getvalue()).decode()
|
| 649 |
|
| 650 |
-
# # 3. At this point it must be a string
|
| 651 |
# if not isinstance(raw_b64, str):
|
| 652 |
# raise TypeError(f"Expected base64 string or PIL Image, got {type(raw_b64)}")
|
| 653 |
|
| 654 |
-
# #
|
| 655 |
# clean_b64 = re.sub(r"^data:image\/[a-zA-Z]+;base64,", "", raw_b64)
|
| 656 |
# clean_b64 = clean_b64.replace("\n", "").replace("\r", "").strip()
|
| 657 |
|
| 658 |
-
# #
|
| 659 |
-
#
|
| 660 |
-
#
|
| 661 |
-
#
|
| 662 |
-
#
|
| 663 |
-
#
|
| 664 |
-
|
| 665 |
-
#
|
| 666 |
-
#
|
| 667 |
-
|
| 668 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 669 |
"""
|
| 670 |
-
|
| 671 |
-
|
|
|
|
|
|
|
| 672 |
"""
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
|
|
|
|
|
|
|
|
|
| 676 |
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
# Load into PIL
|
| 682 |
-
img = Image.open(BytesIO(image_data))
|
| 683 |
-
|
| 684 |
-
low, high = 20, 95 # reasonable JPEG quality range
|
| 685 |
-
best_b64 = None
|
| 686 |
-
best_size_kb = 0
|
| 687 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 688 |
while low <= high:
|
| 689 |
mid = (low + high) // 2
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 694 |
if size_kb <= max_kb:
|
| 695 |
-
|
| 696 |
-
best_b64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
| 697 |
-
best_size_kb = size_kb
|
| 698 |
low = mid + 1
|
| 699 |
else:
|
| 700 |
-
# Too big, try lower
|
| 701 |
high = mid - 1
|
| 702 |
|
| 703 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 704 |
|
| 705 |
-
|
| 706 |
-
def clean_base64_for_model(raw_b64):
|
| 707 |
-
import io, base64, re
|
| 708 |
-
from PIL import Image
|
| 709 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 710 |
if not raw_b64:
|
| 711 |
-
return ""
|
| 712 |
|
| 713 |
if isinstance(raw_b64, list):
|
| 714 |
raw_b64 = raw_b64[0] if raw_b64 else ""
|
| 715 |
if not raw_b64:
|
| 716 |
-
return ""
|
| 717 |
|
| 718 |
if isinstance(raw_b64, Image.Image):
|
| 719 |
buf = io.BytesIO()
|
| 720 |
-
|
| 721 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 722 |
|
| 723 |
if not isinstance(raw_b64, str):
|
| 724 |
raise TypeError(f"Expected base64 string or PIL Image, got {type(raw_b64)}")
|
| 725 |
|
| 726 |
-
#
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 743 |
|
| 744 |
def format_scratch_pseudo_code(code_string):
|
| 745 |
"""
|
|
@@ -879,6 +1073,7 @@ If you find any "Code-Blocks" then,
|
|
| 879 |
|
| 880 |
3. **Pseudo‑code formatting**:
|
| 881 |
- Represent each block or nested block on its own line.
|
|
|
|
| 882 |
- **Indent nested blocks by 4 spaces under their parent (`forever`, `if`, etc.).This is a critical requirement.**
|
| 883 |
- No comments or explanatory text—just the block sequence.
|
| 884 |
- a natural language breakdown of each step taken after the event, formatted as a multi-line string representing pseudo-code. Ensure clarity and granularity—each described action should map closely to a Scratch block or tight sequence.
|
|
@@ -1010,6 +1205,7 @@ If you find any "Code-Blocks" then,
|
|
| 1010 |
logger.info("Plan refinement and block relation analysis completed for all plans.")
|
| 1011 |
return state
|
| 1012 |
|
|
|
|
| 1013 |
def node_optimizer(state: GameState):
|
| 1014 |
logger.info("--- Running Node Optimizer Node ---")
|
| 1015 |
project_json = state["project_json"]
|
|
@@ -1055,7 +1251,7 @@ def node_optimizer(state: GameState):
|
|
| 1055 |
return state
|
| 1056 |
except Exception as e:
|
| 1057 |
logger.error(f"Error in Node Optimizer Node: {e}")
|
| 1058 |
-
|
| 1059 |
# Node 2: planner node
|
| 1060 |
def overall_planner_node(state: GameState):
|
| 1061 |
"""
|
|
@@ -1133,7 +1329,10 @@ Blocks:
|
|
| 1133 |
|
| 1134 |
Your task is to use the `Sprite_name` given and `Pseudo_code` and add it to the specific target name and define the primary actions and movements.
|
| 1135 |
The output should be a JSON object with a single key 'action_overall_flow'. Each key inside this object should be a sprite or 'Stage' name (e.g., 'Player', 'Enemy', 'Stage'), and its value must include a 'description' and a list of 'plans'.
|
|
|
|
|
|
|
| 1136 |
Each plan must include a **single Scratch Hat Block** (e.g., 'event_whenflagclicked') to start scratch project and should contain:
|
|
|
|
| 1137 |
1. **'event'**: the exact `opcode` of the hat block that initiates the logic.
|
| 1138 |
[NOTE: INSTRUCTIONN TO FOLLOW IF PSEUDO_CODE HAVING PROBLEM ]
|
| 1139 |
2. **'logic'**: a natural language breakdown of each step taken after the event, formatted as a multi-line string representing pseudo-code. Ensure clarity and granularity—each described action should map closely to a Scratch block or tight sequence.
|
|
@@ -1295,7 +1494,7 @@ Each plan must include a **single Scratch Hat Block** (e.g., 'event_whenflagclic
|
|
| 1295 |
"operator_lt"
|
| 1296 |
],
|
| 1297 |
"sensing": [
|
| 1298 |
-
"
|
| 1299 |
"sensing_touchingobjectmenu"
|
| 1300 |
],
|
| 1301 |
"looks": [],
|
|
@@ -1431,6 +1630,8 @@ Here is the overall script available:
|
|
| 1431 |
|
| 1432 |
* **Your task is to align to description, refine and correct the JSON object 'action_overall_flow'.**
|
| 1433 |
Use sprite names exactly as provided in `sprite_names` (e.g., 'Sprite1', 'soccer ball'); and also the stage, do **NOT** rename them.
|
|
|
|
|
|
|
| 1434 |
1. **'event'**: the exact `opcode` of the hat block that initiates the logic.
|
| 1435 |
2. **'logic'**: a natural language breakdown of each step taken after the event, formatted as a multi-line string representing pseudo-code. Ensure clarity and granularity—each described action should map closely to a Scratch block or tight sequence.
|
| 1436 |
- Do **NOT** include any justification or comments—only the raw logic.
|
|
@@ -1599,7 +1800,7 @@ Use sprite names exactly as provided in `sprite_names` (e.g., 'Sprite1', 'soccer
|
|
| 1599 |
"operator_lt"
|
| 1600 |
],
|
| 1601 |
"sensing": [
|
| 1602 |
-
"
|
| 1603 |
"sensing_touchingobjectmenu"
|
| 1604 |
],
|
| 1605 |
"looks": [],
|
|
@@ -1806,7 +2007,7 @@ Example output:
|
|
| 1806 |
{{"opcode":"control_forever","count":1}},
|
| 1807 |
{{"opcode":"control_if","count":2}},
|
| 1808 |
{{"opcode":"operator_lt","count":1}},
|
| 1809 |
-
{{"opcode":"
|
| 1810 |
{{"opcode":"event_whenflagclicked","count":1}},
|
| 1811 |
{{"opcode":"event_broadcast","count":1}}
|
| 1812 |
]
|
|
@@ -1843,7 +2044,7 @@ Example output:
|
|
| 1843 |
|
| 1844 |
# Directly use the 'opcode_counts' list from the LLM's output
|
| 1845 |
plan["opcode_counts"] = llm_json.get("opcode_counts", [])
|
| 1846 |
-
|
| 1847 |
# Optionally, you can remove the individual category lists from the plan
|
| 1848 |
# if they are no longer needed after the LLM provides the consolidated list.
|
| 1849 |
# for key in ["motion", "control", "operator", "sensing", "looks", "sounds", "events", "data"]:
|
|
|
|
| 22 |
from io import BytesIO
|
| 23 |
from pathlib import Path
|
| 24 |
import os
|
| 25 |
+
from utils.block_relation_builder import block_builder, separate_scripts, transform_logic_to_action_flow, analyze_opcode_counts
|
| 26 |
from langchain.chat_models import ChatOpenAI
|
| 27 |
from langchain_openai import ChatOpenAI
|
| 28 |
from pydantic import Field, SecretStr
|
| 29 |
+
from difflib import get_close_matches
|
| 30 |
|
| 31 |
os.environ["OPENROUTER_API_KEY"] = os.getenv("OPENROUTER_API_KEY", "default_key_or_placeholder")
|
| 32 |
class ChatOpenRouter(ChatOpenAI):
|
|
|
|
| 166 |
|
| 167 |
# Refined SYSTEM_PROMPT with more explicit Scratch JSON rules, especially for variables
|
| 168 |
SYSTEM_PROMPT = """
|
| 169 |
+
You are GameScratchAgent, an expert in Scratch 3.0 game development.
|
| 170 |
+
Your task is to process OCR-extracted text from images of Scratch 3.0 code blocks and produce **precisely formatted pseudocode JSON**.
|
| 171 |
+
|
| 172 |
+
### Core Role
|
| 173 |
+
- Treat this as an OCR refinement task: the input may contain typos, spacing issues, or incomplete tokens.
|
| 174 |
+
- Intelligently correct such OCR mistakes to align with valid Scratch 3.0 block syntax.
|
| 175 |
+
- Always generate logically valid pseudocode consistent with Scratch semantics.
|
| 176 |
+
|
| 177 |
+
### Responsibilities
|
| 178 |
+
1. **Code-block detection**
|
| 179 |
+
- If no Scratch code-blocks are detected → return `"No Code-blocks"`.
|
| 180 |
+
- If code-blocks are present → refine into pseudocode.
|
| 181 |
+
|
| 182 |
+
2. **Script ownership**
|
| 183 |
+
- Extract the value from `"Script for:"`.
|
| 184 |
+
- If it exactly matches any costume in `Stage_costumes`, set `"name_variable": "Stage"`.
|
| 185 |
+
- Otherwise, keep the detected target name.
|
| 186 |
+
|
| 187 |
+
3. **Pseudocode refinement**
|
| 188 |
+
- Correct OCR artifacts automatically (e.g., “when cliked” → “when green flag clicked”).
|
| 189 |
+
- Apply strict Scratch rules for variables, values, dropdowns, reporters, and booleans.
|
| 190 |
+
- Ensure indentation of nested blocks (4 spaces).
|
| 191 |
+
- Every hat block must end with `end`.
|
| 192 |
+
- Do not include explanations or comments.
|
| 193 |
+
|
| 194 |
+
4. **Formatting precautions**
|
| 195 |
+
- Numbers → `(5)`, `(-130)`
|
| 196 |
+
- Text/strings → `(hello)`
|
| 197 |
+
- Variables → `[score v]`
|
| 198 |
+
- Dropdowns → `[space v]`, `[Game Over v]`
|
| 199 |
+
- Reporter blocks → `((x position))`
|
| 200 |
+
- Boolean logic → `<condition>`, `<<cond1> and <cond2>>`, `<not <cond>>`
|
| 201 |
+
- Operators → explicit, e.g. `(([speed v]) * (1.1))`
|
| 202 |
+
|
| 203 |
+
### Critical Notes
|
| 204 |
+
- Be robust to OCR noise: missing characters, misread symbols, or accidental merges.
|
| 205 |
+
- Never output raw OCR mistakes—always normalize to valid Scratch pseudocode.
|
| 206 |
+
- Output only the JSON object, nothing else.
|
| 207 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
"""
|
| 209 |
|
| 210 |
+
# SYSTEM_PROMPT = """
|
| 211 |
+
# You are an expert AI assistant named GameScratchAgent, specialized in generating and modifying Scratch-VM 3.x game project JSON.
|
| 212 |
+
# Your core task is to process game descriptions and existing Scratch JSON structures, then produce or update JSON segments accurately.
|
| 213 |
+
# You possess deep knowledge of Scratch 3.0 project schema, informed by comprehensive reference materials. When generating or modifying the `blocks` section, pay extremely close attention to the following:
|
| 214 |
+
|
| 215 |
+
# **Scratch Project JSON Schema Rules:**
|
| 216 |
+
|
| 217 |
+
# 1. **Target Structure (`project.json`'s `targets` array):**
|
| 218 |
+
# * Each object in the `targets` array represents a Stage or a Sprite.
|
| 219 |
+
# * `isStage`: A boolean indicating if the target is the Stage (`true`) or a Sprite (`false`).
|
| 220 |
+
# * `name`: The name of the Stage (e.g., `"Stage"`) or the Sprite (e.g., `"Cat"`). This property replaces `objName` found in older Scratch versions.
|
| 221 |
+
# * `variables` dictionary: This dictionary maps unique variable IDs to arrays `[variable_name, initial_value, isCloudVariable?]`.
|
| 222 |
+
# * `variable_name`: The user-defined name of the variable.
|
| 223 |
+
# * `initial_value`: The variable's initial value, which can be a number or a string.
|
| 224 |
+
# * `isCloudVariable?`: (Optional) A boolean indicating if it's a cloud variable (`true`) or a local variable (`false` or absent for regular variables).
|
| 225 |
+
# * Example: `"myVarId123": ["score", 0]`, `"cloudVarId456": ["☁ High Score", "54", true]`
|
| 226 |
+
# * `lists` dictionary: This dictionary maps unique list IDs to arrays `[list_name, [item1, item2, ...]]`.
|
| 227 |
+
# * Example: `"myListId789": ["my list", ["apple", "banana"]]`
|
| 228 |
+
# * `broadcasts` dictionary: This dictionary maps unique broadcast IDs to their names.
|
| 229 |
+
# * Example: `"myBroadcastId": "Game Over"`
|
| 230 |
+
# * `blocks` dictionary: This dictionary contains all the blocks belonging to this target. Keys are block IDs, values are block objects.
|
| 231 |
+
|
| 232 |
+
# 2. **Block Structure (within a `target`'s `blocks` dictionary):**
|
| 233 |
+
# * Every block object must have the following core properties:
|
| 234 |
+
# * [cite_start]`opcode`: A unique internal identifier for the block's specific functionality (e.g., `"motion_movesteps"`, `"event_whenflagclicked"`)[cite: 31, 18, 439, 452].
|
| 235 |
+
# * `parent`: The ID of the block directly above it in the script stack (or `null` for a top-level block).
|
| 236 |
+
# * `next`: The ID of the block directly below it in the script stack (or `null` for the end of a stack).
|
| 237 |
+
# * `inputs`: An object defining values or blocks plugged into the block's input slots. Values are **arrays**.
|
| 238 |
+
# * `fields`: An object defining dropdown menu selections or direct internal values within the block. Values are **arrays**.
|
| 239 |
+
# * `shadow`: `true` if it's a shadow block (e.g., a default number input that can be replaced by another block), `false` otherwise.
|
| 240 |
+
# * `topLevel`: `true` if it's a hat block or a standalone block (not connected to a parent), `false` otherwise.
|
| 241 |
+
|
| 242 |
+
# 3. **`inputs` Property Details (for blocks plugged into input slots):**
|
| 243 |
+
# * **Direct Block Connection (Reporter/Boolean block plugged in):**
|
| 244 |
+
# * Format: `"<INPUT_NAME>": [1, "<blockId_of_plugged_block>"]`
|
| 245 |
+
# * Example: `"CONDITION": [1, "someBooleanBlockId"]` (e.g., for an `if` block).
|
| 246 |
+
# * **Literal Value Input (Shadow block with a literal):**
|
| 247 |
+
# * Format: `"<INPUT_NAME>": [1, [<type_code>, "<value_string>"]]`
|
| 248 |
+
# * `type_code`: A numeric code representing the data type. Common codes include: `4` for number, `7` for string/text, `10` for string/message.
|
| 249 |
+
# * `value_string`: The literal value as a string.
|
| 250 |
+
# * Examples:
|
| 251 |
+
# * Number: `"STEPS": [1, [4, "10"]]` (for `move 10 steps` block).
|
| 252 |
+
# * String/Text: `"MESSAGE": [1, [7, "Hello"]]` (for `say Hello` block).
|
| 253 |
+
# * String/Message (common for text inputs): `"MESSAGE": [1, [10, "Hello!"]]` (for `say Hello! for 2 secs`).
|
| 254 |
+
# * **C-Block Substack (blocks within a loop or conditional):**
|
| 255 |
+
# * Format: `"<SUBSTACK_NAME>": [2, "<blockId_of_first_block_in_substack>"]`
|
| 256 |
+
# * Common `SUBSTACK_NAME` values are `SUBSTACK` (for `if`, `forever`, `repeat`) and `SUBSTACK2` (for `else` in `if else`).
|
| 257 |
+
# * Example: `"SUBSTACK": [2, "firstBlockInLoopId"]`
|
| 258 |
+
|
| 259 |
+
# 4. **`fields` Property Details (for dropdowns or direct internal values):**
|
| 260 |
+
# * Used for dropdown menus, variable names, list names, or other static selections directly within the block.
|
| 261 |
+
# * Format: `"<FIELD_NAME>": ["<selected_value>", null]`
|
| 262 |
+
# * Examples:
|
| 263 |
+
# * Dropdown: `"KEY_OPTION": ["space", null]` (for `when space key pressed`).
|
| 264 |
+
# * Variable Name: `"VARIABLE": ["score", null]` (for `set score to 0`).
|
| 265 |
+
# * Direction (specific motion block): `"FORWARD_BACKWARD": ["forward", null]` (for `go forward layers`).
|
| 266 |
+
|
| 267 |
+
# 5. **Unique IDs:**
|
| 268 |
+
# * All block IDs, variable IDs, and list IDs must be unique strings (e.g., "myBlock123", "myVarId456", "myListId789"). Do NOT use placeholder strings like "block_id_here".
|
| 269 |
+
|
| 270 |
+
# 6. **No Nested `blocks` Dictionary:**
|
| 271 |
+
# * The `blocks` dictionary should only appear once per `target` (sprite/stage). Do NOT nest a `blocks` dictionary inside an individual block definition. Blocks that are part of a substack are linked via the `SUBSTACK` input.
|
| 272 |
+
|
| 273 |
+
# 7. **Asset Properties (for Costumes/Sounds):**
|
| 274 |
+
# * `assetId`, `md5ext`, `bitmapResolution`, `rotationCenterX`/`rotationCenterY` should be correctly associated with costume and sound objects within the `costumes` and `sounds` arrays.
|
| 275 |
+
|
| 276 |
+
# **General Principles and Important Considerations:**
|
| 277 |
+
# * **Backward Compatibility:** Adhere strictly to existing Scratch 3.0 opcodes and schema to ensure backward compatibility with older projects. [cite_start]Opcodes must remain consistent to prevent previously saved projects from failing to load or behaving unexpectedly[cite: 18, 19, 25, 65].
|
| 278 |
+
# * **Forgiving Inputs:** Recognize that Scratch is designed to be "forgiving in its interpretation of inputs." [cite_start]The Scratch VM handles potentially "invalid" inputs gracefully (e.g., converting a number to a string if expected, returning default values like zero or empty strings, or performing no action) rather than crashing[cite: 20, 21, 22, 38, 39, 41]. This implies that precise type matching for inputs might be handled internally by Scratch, allowing for some flexibility in how values are provided, but the agent should aim for the most common and logical type.
|
| 279 |
+
# """
|
| 280 |
+
|
| 281 |
SYSTEM_PROMPT_JSON_CORRECTOR ="""
|
| 282 |
You are an assistant that outputs JSON responses strictly following the given schema.
|
| 283 |
If the JSON you produce has any formatting errors, missing required fields, or invalid structure, you must identify the problems and correct them.
|
|
|
|
| 297 |
tools=[], # No specific tools are defined here, but could be added later
|
| 298 |
prompt=SYSTEM_PROMPT
|
| 299 |
)
|
|
|
|
| 300 |
agent_2 = create_react_agent(
|
| 301 |
model=llm2,
|
| 302 |
tools=[], # No specific tools are defined here, but could be added later
|
|
|
|
| 545 |
#hat_description = hat_block_data.get("description", "No description available")
|
| 546 |
# hat_opcodes_functionalities = "\n".join([f" - Opcode: {block['op_code']}, functionality: {block['functionality']} example: standalone use: {block['example_standalone']}" for block in hat_block_data["blocks"]])
|
| 547 |
hat_opcodes_functionalities = "\n".join([
|
| 548 |
+
# f" - Opcode: {block.get('op_code', 'N/A')}, functionality: {block.get('functionality', 'N/A')}, example: standalone use {block.get('example_standalone', 'N/A')}"
|
| 549 |
+
f" - Opcode: {block.get('op_code', 'N/A')}, example: standalone use {block.get('example_standalone', 'N/A')}"
|
| 550 |
for block in hat_block_data.get("blocks", [])
|
| 551 |
]) if isinstance(hat_block_data.get("blocks"), list) else " No blocks information available."
|
| 552 |
+
#hat_opcodes_functionalities = os.path.join(BLOCKS_DIR, "hat_blocks.txt")
|
| 553 |
print("Hat blocks loaded successfully.", hat_description)
|
| 554 |
|
| 555 |
boolean_block_data = _load_block_catalog(BOOLEAN_BLOCKS_PATH)
|
| 556 |
boolean_description = boolean_block_data["description"]
|
| 557 |
# boolean_opcodes_functionalities = "\n".join([f" - Opcode: {block['op_code']}, functionality: {block['functionality']} example: standalone use: {block['example_standalone']}" for block in boolean_block_data["blocks"]])
|
| 558 |
boolean_opcodes_functionalities = "\n".join([
|
| 559 |
+
# f" - Opcode: {block.get('op_code', 'N/A')}, functionality: {block.get('functionality', 'N/A')}, example: standalone use {block.get('example_standalone', 'N/A')}"
|
| 560 |
+
f" - Opcode: {block.get('op_code', 'N/A')}, example: standalone use {block.get('example_standalone', 'N/A')}"
|
| 561 |
for block in boolean_block_data.get("blocks", [])
|
| 562 |
]) if isinstance(boolean_block_data.get("blocks"), list) else " No blocks information available."
|
| 563 |
+
#boolean_opcodes_functionalities = os.path.join(BLOCKS_DIR, "boolean_blocks.txt")
|
| 564 |
|
| 565 |
c_block_data = _load_block_catalog(C_BLOCKS_PATH)
|
| 566 |
c_description = c_block_data["description"]
|
| 567 |
# c_opcodes_functionalities = "\n".join([f" - Opcode: {block['op_code']}, functionality: {block['functionality']} example: standalone use: {block['example_standalone']}" for block in c_block_data["blocks"]])
|
| 568 |
c_opcodes_functionalities = "\n".join([
|
| 569 |
+
# f" - Opcode: {block.get('op_code', 'N/A')}, functionality: {block.get('functionality', 'N/A')}, example: standalone use {block.get('example_standalone', 'N/A')}"
|
| 570 |
+
f" - Opcode: {block.get('op_code', 'N/A')}, example: standalone use {block.get('example_standalone', 'N/A')}"
|
| 571 |
for block in c_block_data.get("blocks", [])
|
| 572 |
]) if isinstance(c_block_data.get("blocks"), list) else " No blocks information available."
|
| 573 |
+
#c_opcodes_functionalities = os.path.join(BLOCKS_DIR, "c_blocks.txt")
|
| 574 |
|
| 575 |
cap_block_data = _load_block_catalog(CAP_BLOCKS_PATH)
|
| 576 |
cap_description = cap_block_data["description"]
|
| 577 |
# cap_opcodes_functionalities = "\n".join([f" - Opcode: {block['op_code']}, functionality: {block['functionality']} example: standalone use: {block['example_standalone']}" for block in cap_block_data["blocks"]])
|
| 578 |
cap_opcodes_functionalities = "\n".join([
|
| 579 |
+
# f" - Opcode: {block.get('op_code', 'N/A')}, functionality: {block.get('functionality', 'N/A')}, example: standalone use {block.get('example_standalone', 'N/A')}"
|
| 580 |
+
f" - Opcode: {block.get('op_code', 'N/A')}, example: standalone use {block.get('example_standalone', 'N/A')}"
|
| 581 |
for block in cap_block_data.get("blocks", [])
|
| 582 |
]) if isinstance(cap_block_data.get("blocks"), list) else " No blocks information available."
|
| 583 |
+
#cap_opcodes_functionalities = os.path.join(BLOCKS_DIR, "cap_blocks.txt")
|
| 584 |
|
| 585 |
reporter_block_data = _load_block_catalog(REPORTER_BLOCKS_PATH)
|
| 586 |
reporter_description = reporter_block_data["description"]
|
| 587 |
# reporter_opcodes_functionalities = "\n".join([f" - Opcode: {block['op_code']}, functionality: {block['functionality']} example: standalone use: {block['example_standalone']}" for block in reporter_block_data["blocks"]])
|
| 588 |
reporter_opcodes_functionalities = "\n".join([
|
| 589 |
+
# f" - Opcode: {block.get('op_code', 'N/A')}, functionality: {block.get('functionality', 'N/A')}, example: standalone use {block.get('example_standalone', 'N/A')}"
|
| 590 |
+
f" - Opcode: {block.get('op_code', 'N/A')}, example: standalone use {block.get('example_standalone', 'N/A')}"
|
| 591 |
for block in reporter_block_data.get("blocks", [])
|
| 592 |
]) if isinstance(reporter_block_data.get("blocks"), list) else " No blocks information available."
|
| 593 |
+
#reporter_opcodes_functionalities = os.path.join(BLOCKS_DIR, "reporter_blocks.txt")
|
| 594 |
|
| 595 |
stack_block_data = _load_block_catalog(STACK_BLOCKS_PATH)
|
| 596 |
stack_description = stack_block_data["description"]
|
| 597 |
# stack_opcodes_functionalities = "\n".join([f" - Opcode: {block['op_code']}, functionality: {block['functionality']} example: standalone use: {block['example_standalone']}" for block in stack_block_data["blocks"]])
|
| 598 |
stack_opcodes_functionalities = "\n".join([
|
| 599 |
+
# f" - Opcode: {block.get('op_code', 'N/A')}, functionality: {block.get('functionality', 'N/A')}, example: standalone use {block.get('example_standalone', 'N/A')}"
|
| 600 |
+
f" - Opcode: {block.get('op_code', 'N/A')}, example: standalone use {block.get('example_standalone', 'N/A')}"
|
| 601 |
for block in stack_block_data.get("blocks", [])
|
| 602 |
]) if isinstance(stack_block_data.get("blocks"), list) else " No blocks information available."
|
| 603 |
+
#stack_opcodes_functionalities = os.path.join(BLOCKS_DIR, "stack_blocks.txt")
|
| 604 |
|
| 605 |
# This makes ALL_SCRATCH_BLOCKS_CATALOG available globally
|
| 606 |
ALL_SCRATCH_BLOCKS_CATALOG = _load_block_catalog(BLOCK_CATALOG_PATH)
|
|
|
|
| 671 |
logger.error("Sanitized JSON still invalid:\n%s", json_string)
|
| 672 |
raise
|
| 673 |
|
| 674 |
+
# def reduce_image_size_to_limit(clean_b64_str, max_kb=4000):
|
| 675 |
# """
|
| 676 |
+
# Reduce an image's size to be as close as possible to max_kb without exceeding it.
|
| 677 |
+
# Returns the final base64 string and its size in KB.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 678 |
# """
|
| 679 |
+
# import re, base64
|
| 680 |
+
# from io import BytesIO
|
| 681 |
+
# from PIL import Image
|
| 682 |
+
|
| 683 |
+
# # Remove the data URI prefix
|
| 684 |
+
# base64_data = re.sub(r"^data:image\/[a-zA-Z]+;base64,", "", clean_b64_str)
|
| 685 |
+
# image_data = base64.b64decode(base64_data)
|
| 686 |
+
|
| 687 |
+
# # Load into PIL
|
| 688 |
+
# img = Image.open(BytesIO(image_data))
|
| 689 |
+
|
| 690 |
+
# low, high = 20, 95 # reasonable JPEG quality range
|
| 691 |
+
# best_b64 = None
|
| 692 |
+
# best_size_kb = 0
|
| 693 |
+
|
| 694 |
+
# while low <= high:
|
| 695 |
+
# mid = (low + high) // 2
|
| 696 |
+
# buffer = BytesIO()
|
| 697 |
+
# img.save(buffer, format="JPEG", quality=mid)
|
| 698 |
+
# size_kb = len(buffer.getvalue()) / 1024
|
| 699 |
+
|
| 700 |
+
# if size_kb <= max_kb:
|
| 701 |
+
# # This quality is valid, try higher
|
| 702 |
+
# best_b64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
| 703 |
+
# best_size_kb = size_kb
|
| 704 |
+
# low = mid + 1
|
| 705 |
+
# else:
|
| 706 |
+
# # Too big, try lower
|
| 707 |
+
# high = mid - 1
|
| 708 |
+
|
| 709 |
+
# return f"data:image/jpeg;base64,{best_b64}"
|
| 710 |
+
|
| 711 |
+
# #clean the base64 model here
|
| 712 |
+
# def clean_base64_for_model(raw_b64):
|
| 713 |
+
# import io, base64, re
|
| 714 |
+
# from PIL import Image
|
| 715 |
+
|
| 716 |
# if not raw_b64:
|
| 717 |
+
# return "", ""
|
| 718 |
|
|
|
|
| 719 |
# if isinstance(raw_b64, list):
|
| 720 |
# raw_b64 = raw_b64[0] if raw_b64 else ""
|
| 721 |
# if not raw_b64:
|
| 722 |
+
# return "", ""
|
| 723 |
|
|
|
|
| 724 |
# if isinstance(raw_b64, Image.Image):
|
| 725 |
# buf = io.BytesIO()
|
| 726 |
# raw_b64.save(buf, format="PNG")
|
| 727 |
# raw_b64 = base64.b64encode(buf.getvalue()).decode()
|
| 728 |
|
|
|
|
| 729 |
# if not isinstance(raw_b64, str):
|
| 730 |
# raise TypeError(f"Expected base64 string or PIL Image, got {type(raw_b64)}")
|
| 731 |
|
| 732 |
+
# # Remove data URI prefix if present
|
| 733 |
# clean_b64 = re.sub(r"^data:image\/[a-zA-Z]+;base64,", "", raw_b64)
|
| 734 |
# clean_b64 = clean_b64.replace("\n", "").replace("\r", "").strip()
|
| 735 |
|
| 736 |
+
# # Log original size
|
| 737 |
+
# original_size = len(clean_b64.encode("utf-8"))
|
| 738 |
+
# print(f"Original Base64 size (bytes): {original_size}")
|
| 739 |
+
# if original_size > 4000000:
|
| 740 |
+
# # Reduce size to under 4 MB
|
| 741 |
+
# reduced_b64 = reduce_image_size_to_limit(clean_b64, max_kb=4000)
|
| 742 |
+
# clean_b64_2 = re.sub(r"^data:image\/[a-zA-Z]+;base64,", "", reduced_b64)
|
| 743 |
+
# clean_b64_2 = clean_b64_2.replace("\n", "").replace("\r", "").strip()
|
| 744 |
+
# reduced_size = len(clean_b64_2.encode("utf-8"))
|
| 745 |
+
# print(f"Reduced Base64 size (bytes): {reduced_size}")
|
| 746 |
+
# # Return both prefixed and clean reduced versions
|
| 747 |
+
# return f"data:image/jpeg;base64,{reduced_b64}"
|
| 748 |
+
# return f"data:image/jpeg;base64,{clean_b64}"
|
| 749 |
+
|
| 750 |
+
def reduce_image_size_to_limit(clean_b64_str: str, max_kb: int = 4000) -> str:
|
| 751 |
"""
|
| 752 |
+
Input: clean_b64_str = BASE64 STRING (no data: prefix)
|
| 753 |
+
Output: BASE64 STRING (no data: prefix), sized as close as possible to max_kb KB.
|
| 754 |
+
Guarantees: returns a valid base64 string (never None). May still be larger than max_kb
|
| 755 |
+
if saving at lowest quality cannot get under the limit.
|
| 756 |
"""
|
| 757 |
+
# sanitize
|
| 758 |
+
clean = re.sub(r"\s+", "", clean_b64_str).strip()
|
| 759 |
+
# fix padding
|
| 760 |
+
missing = len(clean) % 4
|
| 761 |
+
if missing:
|
| 762 |
+
clean += "=" * (4 - missing)
|
| 763 |
|
| 764 |
+
try:
|
| 765 |
+
image_data = base64.b64decode(clean)
|
| 766 |
+
except Exception as e:
|
| 767 |
+
raise ValueError("Invalid base64 input to reduce_image_size_to_limit") from e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 768 |
|
| 769 |
+
try:
|
| 770 |
+
img = Image.open(io.BytesIO(image_data))
|
| 771 |
+
img.load()
|
| 772 |
+
except Exception as e:
|
| 773 |
+
raise ValueError("Could not open image from base64") from e
|
| 774 |
+
|
| 775 |
+
# convert alpha -> RGB because JPEG doesn't support alpha
|
| 776 |
+
if img.mode in ("RGBA", "LA") or (img.mode == "P" and "transparency" in img.info):
|
| 777 |
+
background = Image.new("RGB", img.size, (255, 255, 255))
|
| 778 |
+
background.paste(img, mask=img.split()[-1] if img.mode != "RGB" else None)
|
| 779 |
+
img = background
|
| 780 |
+
elif img.mode != "RGB":
|
| 781 |
+
img = img.convert("RGB")
|
| 782 |
+
|
| 783 |
+
low, high = 20, 95
|
| 784 |
+
best_bytes = None
|
| 785 |
+
# binary search for best quality
|
| 786 |
while low <= high:
|
| 787 |
mid = (low + high) // 2
|
| 788 |
+
buf = io.BytesIO()
|
| 789 |
+
try:
|
| 790 |
+
img.save(buf, format="JPEG", quality=mid, optimize=True)
|
| 791 |
+
except OSError:
|
| 792 |
+
# some PIL builds/channels may throw on optimize=True; fallback without optimize
|
| 793 |
+
buf = io.BytesIO()
|
| 794 |
+
img.save(buf, format="JPEG", quality=mid)
|
| 795 |
+
size_kb = len(buf.getvalue()) / 1024.0
|
| 796 |
if size_kb <= max_kb:
|
| 797 |
+
best_bytes = buf.getvalue()
|
|
|
|
|
|
|
| 798 |
low = mid + 1
|
| 799 |
else:
|
|
|
|
| 800 |
high = mid - 1
|
| 801 |
|
| 802 |
+
# if never found a quality <= max_kb, use the smallest we created (quality = 20)
|
| 803 |
+
if best_bytes is None:
|
| 804 |
+
buf = io.BytesIO()
|
| 805 |
+
try:
|
| 806 |
+
img.save(buf, format="JPEG", quality=20, optimize=True)
|
| 807 |
+
except OSError:
|
| 808 |
+
buf = io.BytesIO()
|
| 809 |
+
img.save(buf, format="JPEG", quality=20)
|
| 810 |
+
best_bytes = buf.getvalue()
|
| 811 |
|
| 812 |
+
return base64.b64encode(best_bytes).decode("utf-8")
|
|
|
|
|
|
|
|
|
|
| 813 |
|
| 814 |
+
|
| 815 |
+
def clean_base64_for_model(raw_b64, max_bytes_threshold=4000000) -> str:
|
| 816 |
+
"""
|
| 817 |
+
Accepts: raw_b64 can be:
|
| 818 |
+
- a data URI 'data:image/png;base64,...'
|
| 819 |
+
- a plain base64 string
|
| 820 |
+
- a PIL Image
|
| 821 |
+
- a list containing the above (take first)
|
| 822 |
+
Returns: a data URI string 'data:<mime>;base64,<base64>' guaranteed to be syntactically valid.
|
| 823 |
+
"""
|
| 824 |
+
# normalize input
|
| 825 |
if not raw_b64:
|
| 826 |
+
return ""
|
| 827 |
|
| 828 |
if isinstance(raw_b64, list):
|
| 829 |
raw_b64 = raw_b64[0] if raw_b64 else ""
|
| 830 |
if not raw_b64:
|
| 831 |
+
return ""
|
| 832 |
|
| 833 |
if isinstance(raw_b64, Image.Image):
|
| 834 |
buf = io.BytesIO()
|
| 835 |
+
# convert to RGB and save as JPEG to keep consistent
|
| 836 |
+
img = raw_b64.convert("RGB")
|
| 837 |
+
img.save(buf, format="JPEG")
|
| 838 |
+
clean_b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
|
| 839 |
+
mime = "image/jpeg"
|
| 840 |
+
return f"data:{mime};base64,{clean_b64}"
|
| 841 |
|
| 842 |
if not isinstance(raw_b64, str):
|
| 843 |
raise TypeError(f"Expected base64 string or PIL Image, got {type(raw_b64)}")
|
| 844 |
|
| 845 |
+
# detect mime if present; otherwise default to png
|
| 846 |
+
m = re.match(r"^data:(image\/[a-zA-Z0-9.+-]+);base64,(.+)$", raw_b64, flags=re.DOTALL)
|
| 847 |
+
if m:
|
| 848 |
+
mime = m.group(1)
|
| 849 |
+
clean_b64 = m.group(2)
|
| 850 |
+
else:
|
| 851 |
+
# no prefix; assume png by default (you can change to jpeg if you prefer)
|
| 852 |
+
mime = "image/png"
|
| 853 |
+
clean_b64 = raw_b64
|
| 854 |
+
|
| 855 |
+
# sanitize base64 string
|
| 856 |
+
clean_b64 = re.sub(r"\s+", "", clean_b64).strip()
|
| 857 |
+
missing = len(clean_b64) % 4
|
| 858 |
+
if missing:
|
| 859 |
+
clean_b64 += "=" * (4 - missing)
|
| 860 |
+
|
| 861 |
+
original_size_bytes = len(clean_b64.encode("utf-8"))
|
| 862 |
+
# debug print
|
| 863 |
+
print(f"Original base64 size (bytes): {original_size_bytes}, mime: {mime}")
|
| 864 |
+
|
| 865 |
+
if original_size_bytes > max_bytes_threshold:
|
| 866 |
+
# reduce and return JPEG prefixed data URI (JPEG tends to compress better for photos)
|
| 867 |
+
reduced_clean = reduce_image_size_to_limit(clean_b64, max_kb=4000)
|
| 868 |
+
# reduced_clean is plain base64 (no prefix)
|
| 869 |
+
print(f"Reduced base64 size (bytes): {original_size_bytes}, mime: {mime}")
|
| 870 |
+
return f"data:image/jpeg;base64,{reduced_clean}"
|
| 871 |
+
|
| 872 |
+
# otherwise return original with its mime prefix (ensure prefix exists)
|
| 873 |
+
return f"data:{mime};base64,{clean_b64}"
|
| 874 |
+
|
| 875 |
+
|
| 876 |
+
SCRATCH_OPCODES = [
|
| 877 |
+
'motion_movesteps', 'motion_turnright', 'motion_turnleft', 'motion_goto',
|
| 878 |
+
'motion_gotoxy', 'motion_glideto', 'motion_glidesecstoxy', 'motion_pointindirection',
|
| 879 |
+
'motion_pointtowards', 'motion_changexby', 'motion_setx', 'motion_changeyby',
|
| 880 |
+
'motion_sety', 'motion_ifonedgebounce', 'motion_setrotationstyle', 'looks_sayforsecs',
|
| 881 |
+
'looks_say', 'looks_thinkforsecs', 'looks_think', 'looks_switchcostumeto',
|
| 882 |
+
'looks_nextcostume', 'looks_switchbackdropto', 'looks_switchbackdroptowait',
|
| 883 |
+
'looks_nextbackdrop', 'looks_changesizeby', 'looks_setsizeto', 'looks_changeeffectby',
|
| 884 |
+
'looks_seteffectto', 'looks_cleargraphiceffects', 'looks_show', 'looks_hide',
|
| 885 |
+
'looks_gotofrontback', 'looks_goforwardbackwardlayers', 'sound_playuntildone',
|
| 886 |
+
'sound_play', 'sound_stopallsounds', 'sound_changevolumeby', 'sound_setvolumeto',
|
| 887 |
+
'event_broadcast', 'event_broadcastandwait', 'control_wait', 'control_wait_until',
|
| 888 |
+
'control_stop', 'control_create_clone_of', 'control_delete_this_clone',
|
| 889 |
+
'data_setvariableto', 'data_changevariableby', 'data_addtolist', 'data_deleteoflist',
|
| 890 |
+
'data_insertatlist', 'data_replaceitemoflist', 'data_showvariable', 'data_hidevariable',
|
| 891 |
+
'data_showlist', 'data_hidelist', 'sensing_askandwait', 'sensing_resettimer',
|
| 892 |
+
'sensing_setdragmode', 'procedures_call', 'operator_lt', 'operator_equals',
|
| 893 |
+
'operator_gt', 'operator_and', 'operator_or', 'operator_not', 'operator_contains',
|
| 894 |
+
'sensing_touchingobject', 'sensing_touchingcolor', 'sensing_coloristouchingcolor',
|
| 895 |
+
'sensing_keypressed', 'sensing_mousedown', 'data_listcontainsitem', 'control_repeat',
|
| 896 |
+
'control_forever', 'control_if', 'control_if_else', 'control_repeat_until',
|
| 897 |
+
'motion_xposition', 'motion_yposition', 'motion_direction', 'looks_costumenumbername',
|
| 898 |
+
'looks_size', 'looks_backdropnumbername', 'sound_volume', 'sensing_distanceto',
|
| 899 |
+
'sensing_answer', 'sensing_mousex', 'sensing_mousey', 'sensing_loudness',
|
| 900 |
+
'sensing_timer', 'sensing_of', 'sensing_current', 'sensing_dayssince2000',
|
| 901 |
+
'sensing_username', 'operator_add', 'operator_subtract', 'operator_multiply',
|
| 902 |
+
'operator_divide', 'operator_random', 'operator_join', 'operator_letterof',
|
| 903 |
+
'operator_length', 'operator_mod', 'operator_round', 'operator_mathop',
|
| 904 |
+
'data_variable', 'data_list', 'data_itemoflist', 'data_lengthoflist',
|
| 905 |
+
'data_itemnumoflist', 'event_whenflagclicked', 'event_whenkeypressed',
|
| 906 |
+
'event_whenthisspriteclicked', 'event_whenbackdropswitchesto', 'event_whengreaterthan',
|
| 907 |
+
'event_whenbroadcastreceived', 'control_start_as_clone', 'procedures_definition'
|
| 908 |
+
]
|
| 909 |
+
|
| 910 |
+
def validate_and_fix_opcodes(opcode_counts):
|
| 911 |
+
"""
|
| 912 |
+
Ensures all opcodes are valid. If an opcode is invalid, replace with closest match.
|
| 913 |
+
"""
|
| 914 |
+
corrected_list = []
|
| 915 |
+
for item in opcode_counts:
|
| 916 |
+
opcode = item.get("opcode")
|
| 917 |
+
count = item.get("count", 1)
|
| 918 |
+
|
| 919 |
+
if opcode not in SCRATCH_OPCODES:
|
| 920 |
+
# Find closest match (case-sensitive)
|
| 921 |
+
match = get_close_matches(opcode, SCRATCH_OPCODES, n=1, cutoff=0.6)
|
| 922 |
+
if match:
|
| 923 |
+
print(f"Opcode '{opcode}' not found. Replacing with '{match[0]}'")
|
| 924 |
+
opcode = match[0]
|
| 925 |
+
else:
|
| 926 |
+
print(f"Opcode '{opcode}' not recognized and no close match found. Skipping.")
|
| 927 |
+
continue
|
| 928 |
+
|
| 929 |
+
corrected_list.append({"opcode": opcode, "count": count})
|
| 930 |
+
|
| 931 |
+
# Merge duplicates after correction
|
| 932 |
+
merged = {}
|
| 933 |
+
for item in corrected_list:
|
| 934 |
+
merged[item["opcode"]] = merged.get(item["opcode"], 0) + item["count"]
|
| 935 |
+
|
| 936 |
+
return [{"opcode": k, "count": v} for k, v in merged.items()]
|
| 937 |
|
| 938 |
def format_scratch_pseudo_code(code_string):
|
| 939 |
"""
|
|
|
|
| 1073 |
|
| 1074 |
3. **Pseudo‑code formatting**:
|
| 1075 |
- Represent each block or nested block on its own line.
|
| 1076 |
+
- Auto-correct invalid OCR phrases to valid Scratch 3.0 block names using the **Scratch 3.0 Block Reference** above.
|
| 1077 |
- **Indent nested blocks by 4 spaces under their parent (`forever`, `if`, etc.).This is a critical requirement.**
|
| 1078 |
- No comments or explanatory text—just the block sequence.
|
| 1079 |
- a natural language breakdown of each step taken after the event, formatted as a multi-line string representing pseudo-code. Ensure clarity and granularity—each described action should map closely to a Scratch block or tight sequence.
|
|
|
|
| 1205 |
logger.info("Plan refinement and block relation analysis completed for all plans.")
|
| 1206 |
return state
|
| 1207 |
|
| 1208 |
+
# Node: Node Optimizer node
|
| 1209 |
def node_optimizer(state: GameState):
|
| 1210 |
logger.info("--- Running Node Optimizer Node ---")
|
| 1211 |
project_json = state["project_json"]
|
|
|
|
| 1251 |
return state
|
| 1252 |
except Exception as e:
|
| 1253 |
logger.error(f"Error in Node Optimizer Node: {e}")
|
| 1254 |
+
|
| 1255 |
# Node 2: planner node
|
| 1256 |
def overall_planner_node(state: GameState):
|
| 1257 |
"""
|
|
|
|
| 1329 |
|
| 1330 |
Your task is to use the `Sprite_name` given and `Pseudo_code` and add it to the specific target name and define the primary actions and movements.
|
| 1331 |
The output should be a JSON object with a single key 'action_overall_flow'. Each key inside this object should be a sprite or 'Stage' name (e.g., 'Player', 'Enemy', 'Stage'), and its value must include a 'description' and a list of 'plans'.
|
| 1332 |
+
The first plan in each stage must start with one Scratch Hat Block (e.g., event_whenflagclicked).
|
| 1333 |
+
Other plans may use any hat blocks (including duplicates) to represent different logics or events.
|
| 1334 |
Each plan must include a **single Scratch Hat Block** (e.g., 'event_whenflagclicked') to start scratch project and should contain:
|
| 1335 |
+
|
| 1336 |
1. **'event'**: the exact `opcode` of the hat block that initiates the logic.
|
| 1337 |
[NOTE: INSTRUCTIONN TO FOLLOW IF PSEUDO_CODE HAVING PROBLEM ]
|
| 1338 |
2. **'logic'**: a natural language breakdown of each step taken after the event, formatted as a multi-line string representing pseudo-code. Ensure clarity and granularity—each described action should map closely to a Scratch block or tight sequence.
|
|
|
|
| 1494 |
"operator_lt"
|
| 1495 |
],
|
| 1496 |
"sensing": [
|
| 1497 |
+
"sensing_touchingobject",
|
| 1498 |
"sensing_touchingobjectmenu"
|
| 1499 |
],
|
| 1500 |
"looks": [],
|
|
|
|
| 1630 |
|
| 1631 |
* **Your task is to align to description, refine and correct the JSON object 'action_overall_flow'.**
|
| 1632 |
Use sprite names exactly as provided in `sprite_names` (e.g., 'Sprite1', 'soccer ball'); and also the stage, do **NOT** rename them.
|
| 1633 |
+
Other plans may use any hat blocks (including duplicates) to represent different logics or events.
|
| 1634 |
+
Each plan must include a **single Scratch Hat Block** (e.g., 'event_whenflagclicked') to start scratch project and should contain:
|
| 1635 |
1. **'event'**: the exact `opcode` of the hat block that initiates the logic.
|
| 1636 |
2. **'logic'**: a natural language breakdown of each step taken after the event, formatted as a multi-line string representing pseudo-code. Ensure clarity and granularity—each described action should map closely to a Scratch block or tight sequence.
|
| 1637 |
- Do **NOT** include any justification or comments—only the raw logic.
|
|
|
|
| 1800 |
"operator_lt"
|
| 1801 |
],
|
| 1802 |
"sensing": [
|
| 1803 |
+
"sensing_touchingobject",
|
| 1804 |
"sensing_touchingobjectmenu"
|
| 1805 |
],
|
| 1806 |
"looks": [],
|
|
|
|
| 2007 |
{{"opcode":"control_forever","count":1}},
|
| 2008 |
{{"opcode":"control_if","count":2}},
|
| 2009 |
{{"opcode":"operator_lt","count":1}},
|
| 2010 |
+
{{"opcode":"sensing_touchingobject","count":1}},
|
| 2011 |
{{"opcode":"event_whenflagclicked","count":1}},
|
| 2012 |
{{"opcode":"event_broadcast","count":1}}
|
| 2013 |
]
|
|
|
|
| 2044 |
|
| 2045 |
# Directly use the 'opcode_counts' list from the LLM's output
|
| 2046 |
plan["opcode_counts"] = llm_json.get("opcode_counts", [])
|
| 2047 |
+
plan["opcode_counts"] = validate_and_fix_opcodes(plan["opcode_counts"])
|
| 2048 |
# Optionally, you can remove the individual category lists from the plan
|
| 2049 |
# if they are no longer needed after the LLM provides the consolidated list.
|
| 2050 |
# for key in ["motion", "control", "operator", "sensing", "looks", "sounds", "events", "data"]:
|