prthm11 commited on
Commit
e79c7d9
·
verified ·
1 Parent(s): 38ca3ae

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +361 -160
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#, variable_adder_main
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 AI assistant named GameScratchAgent, specialized in generating and modifying Scratch-VM 3.x game project JSON.
169
- Your core task is to process game descriptions and existing Scratch JSON structures, then produce or update JSON segments accurately.
170
- 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:
171
-
172
- **Scratch Project JSON Schema Rules:**
173
-
174
- 1. **Target Structure (`project.json`'s `targets` array):**
175
- * Each object in the `targets` array represents a Stage or a Sprite.
176
- * `isStage`: A boolean indicating if the target is the Stage (`true`) or a Sprite (`false`).
177
- * `name`: The name of the Stage (e.g., `"Stage"`) or the Sprite (e.g., `"Cat"`). This property replaces `objName` found in older Scratch versions.
178
- * `variables` dictionary: This dictionary maps unique variable IDs to arrays `[variable_name, initial_value, isCloudVariable?]`.
179
- * `variable_name`: The user-defined name of the variable.
180
- * `initial_value`: The variable's initial value, which can be a number or a string.
181
- * `isCloudVariable?`: (Optional) A boolean indicating if it's a cloud variable (`true`) or a local variable (`false` or absent for regular variables).
182
- * Example: `"myVarId123": ["score", 0]`, `"cloudVarId456": ["☁ High Score", "54", true]`
183
- * `lists` dictionary: This dictionary maps unique list IDs to arrays `[list_name, [item1, item2, ...]]`.
184
- * Example: `"myListId789": ["my list", ["apple", "banana"]]`
185
- * `broadcasts` dictionary: This dictionary maps unique broadcast IDs to their names.
186
- * Example: `"myBroadcastId": "Game Over"`
187
- * `blocks` dictionary: This dictionary contains all the blocks belonging to this target. Keys are block IDs, values are block objects.
188
-
189
- 2. **Block Structure (within a `target`'s `blocks` dictionary):**
190
- * Every block object must have the following core properties:
191
- * [cite_start]`opcode`: A unique internal identifier for the block's specific functionality (e.g., `"motion_movesteps"`, `"event_whenflagclicked"`)[cite: 31, 18, 439, 452].
192
- * `parent`: The ID of the block directly above it in the script stack (or `null` for a top-level block).
193
- * `next`: The ID of the block directly below it in the script stack (or `null` for the end of a stack).
194
- * `inputs`: An object defining values or blocks plugged into the block's input slots. Values are **arrays**.
195
- * `fields`: An object defining dropdown menu selections or direct internal values within the block. Values are **arrays**.
196
- * `shadow`: `true` if it's a shadow block (e.g., a default number input that can be replaced by another block), `false` otherwise.
197
- * `topLevel`: `true` if it's a hat block or a standalone block (not connected to a parent), `false` otherwise.
198
-
199
- 3. **`inputs` Property Details (for blocks plugged into input slots):**
200
- * **Direct Block Connection (Reporter/Boolean block plugged in):**
201
- * Format: `"<INPUT_NAME>": [1, "<blockId_of_plugged_block>"]`
202
- * Example: `"CONDITION": [1, "someBooleanBlockId"]` (e.g., for an `if` block).
203
- * **Literal Value Input (Shadow block with a literal):**
204
- * Format: `"<INPUT_NAME>": [1, [<type_code>, "<value_string>"]]`
205
- * `type_code`: A numeric code representing the data type. Common codes include: `4` for number, `7` for string/text, `10` for string/message.
206
- * `value_string`: The literal value as a string.
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 clean_base64_for_model(raw_b64):
627
  # """
628
- # Normalize input into a valid data:image/png;base64,<payload> string.
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
- # # 4. Strip any existing data URI prefix, whitespace, or newlines
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
- # # 5. Validate it’s proper base64
659
- # try:
660
- # base64.b64decode(clean_b64)
661
- # except Exception as e:
662
- # logger.error(f"Invalid Base64 passed to model: {e}")
663
- # raise
664
-
665
- # # 6. Return with the correct data URI prefix
666
- # return f"data:image/png;base64,{clean_b64}"
667
-
668
- def reduce_image_size_to_limit(clean_b64_str, max_kb=4000):
 
 
 
 
669
  """
670
- Reduce an image's size to be as close as possible to max_kb without exceeding it.
671
- Returns the final base64 string and its size in KB.
 
 
672
  """
673
- import re, base64
674
- from io import BytesIO
675
- from PIL import Image
 
 
 
676
 
677
- # Remove the data URI prefix
678
- base64_data = re.sub(r"^data:image\/[a-zA-Z]+;base64,", "", clean_b64_str)
679
- image_data = base64.b64decode(base64_data)
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
- buffer = BytesIO()
691
- img.save(buffer, format="JPEG", quality=mid)
692
- size_kb = len(buffer.getvalue()) / 1024
693
-
 
 
 
 
694
  if size_kb <= max_kb:
695
- # This quality is valid, try higher
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
- return f"data:image/jpeg;base64,{best_b64}"
 
 
 
 
 
 
 
 
704
 
705
- #clean the base64 model here
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
- raw_b64.save(buf, format="PNG")
721
- raw_b64 = base64.b64encode(buf.getvalue()).decode()
 
 
 
 
722
 
723
  if not isinstance(raw_b64, str):
724
  raise TypeError(f"Expected base64 string or PIL Image, got {type(raw_b64)}")
725
 
726
- # Remove data URI prefix if present
727
- clean_b64 = re.sub(r"^data:image\/[a-zA-Z]+;base64,", "", raw_b64)
728
- clean_b64 = clean_b64.replace("\n", "").replace("\r", "").strip()
729
-
730
- # Log original size
731
- original_size = len(clean_b64.encode("utf-8"))
732
- print(f"Original Base64 size (bytes): {original_size}")
733
- if original_size > 4000000:
734
- # Reduce size to under 4 MB
735
- reduced_b64 = reduce_image_size_to_limit(clean_b64, max_kb=4000)
736
- clean_b64_2 = re.sub(r"^data:image\/[a-zA-Z]+;base64,", "", reduced_b64)
737
- clean_b64_2 = clean_b64_2.replace("\n", "").replace("\r", "").strip()
738
- reduced_size = len(clean_b64_2.encode("utf-8"))
739
- print(f"Reduced Base64 size (bytes): {reduced_size}")
740
- # Return both prefixed and clean reduced versions
741
- return f"data:image/jpeg;base64,{reduced_b64}"
742
- return f"data:image/jpeg;base64,{clean_b64}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- "sensing_istouching",
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
- "sensing_istouching",
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":"sensing_istouching","count":1}},
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"]: