prthm11 commited on
Commit
4534ce2
·
verified ·
1 Parent(s): 0b8304e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +162 -504
app.py CHANGED
@@ -141,27 +141,16 @@ for d in (
141
  # JSON_DIR,
142
  ):
143
  d.mkdir(parents=True, exist_ok=True)
144
- # def classify_image_type(description_or_name: str) -> str:
145
- # desc = description_or_name.lower()
146
-
147
- # sprite_keywords = ["sprite", "character", "animal", "person", "creature", "robot", "figure"]
148
- # backdrop_keywords = ["background", "scene", "forest", "city", "room", "sky", "mountain", "village"]
149
- # code_block_keywords = [
150
- # "move", "turn", "wait", "repeat", "if", "else", "broadcast",
151
- # "glide", "change", "forever", "when", "switch", "costume",
152
- # "say", "think", "stop", "clone", "touching", "sensing",
153
- # "scratch", "block", "code", "set", "variable"
154
- # ]
155
-
156
- # if any(kw in desc for kw in code_block_keywords):
157
- # return "code-block"
158
- # elif any(kw in desc for kw in sprite_keywords):
159
- # return "sprite"
160
- # elif any(kw in desc for kw in backdrop_keywords):
161
- # return "backdrop"
162
- # else:
163
- # return "unknown"
164
 
 
 
 
 
 
 
 
 
 
165
  class GameState(TypedDict):
166
  project_json: dict
167
  description: str
@@ -170,10 +159,9 @@ class GameState(TypedDict):
170
  pseudo_code: dict
171
  action_plan: Optional[Dict]
172
  temporary_node: Optional[Dict]
173
-
174
- # class GameState(TypedDict):
175
- # image: str
176
- # pseudo_node: Optional[Dict]
177
 
178
  # Refined SYSTEM_PROMPT with more explicit Scratch JSON rules, especially for variables
179
  SYSTEM_PROMPT = """
@@ -278,25 +266,6 @@ agent_json_resolver = create_react_agent(
278
  prompt=SYSTEM_PROMPT_JSON_CORRECTOR
279
  )
280
 
281
- # # Helper function to load the block catalog from a JSON file
282
- # def _load_block_catalog(file_path: str) -> Dict:
283
- # """Loads the Scratch block catalog from a specified JSON file."""
284
- # try:
285
- # with open(file_path, 'r') as f:
286
- # catalog = json.load(f)
287
- # logger.info(f"Successfully loaded block catalog from {file_path}")
288
- # return catalog
289
- # except FileNotFoundError:
290
- # logger.error(f"Error: Block catalog file not found at {file_path}")
291
- # # Return an empty dict or raise an error, depending on desired behavior
292
- # return {}
293
- # except json.JSONDecodeError as e:
294
- # logger.error(f"Error decoding JSON from {file_path}: {e}")
295
- # return {}
296
- # except Exception as e:
297
- # logger.error(f"An unexpected error occurred while loading {file_path}: {e}")
298
- # return {}
299
-
300
  # Helper function to load the block catalog from a JSON file
301
  def _load_block_catalog(block_type: str) -> Dict:
302
  """
@@ -654,48 +623,124 @@ def extract_json_from_llm_response(raw_response: str) -> dict:
654
  logger.error("Sanitized JSON still invalid:\n%s", json_string)
655
  raise
656
 
657
- def clean_base64_for_model(raw_b64):
658
- """
659
- Normalize input into a valid data:image/png;base64,<payload> string.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
660
 
661
- Accepts:
662
- - a list of base64 strings → picks the first element
663
- - a PIL Image instance → encodes to PNG/base64
664
- - a raw base64 string → strips whitespace and data URI prefix
665
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666
  if not raw_b64:
667
- return ""
668
 
669
- # 1. If it’s a list, take its first element
670
  if isinstance(raw_b64, list):
671
  raw_b64 = raw_b64[0] if raw_b64 else ""
672
  if not raw_b64:
673
- return ""
674
 
675
- # 2. If it’s a PIL Image, convert to base64
676
  if isinstance(raw_b64, Image.Image):
677
  buf = io.BytesIO()
678
  raw_b64.save(buf, format="PNG")
679
  raw_b64 = base64.b64encode(buf.getvalue()).decode()
680
 
681
- # 3. At this point it must be a string
682
  if not isinstance(raw_b64, str):
683
  raise TypeError(f"Expected base64 string or PIL Image, got {type(raw_b64)}")
684
 
685
- # 4. Strip any existing data URI prefix, whitespace, or newlines
686
  clean_b64 = re.sub(r"^data:image\/[a-zA-Z]+;base64,", "", raw_b64)
687
  clean_b64 = clean_b64.replace("\n", "").replace("\r", "").strip()
688
 
689
- # 5. Validate it’s proper base64
690
- try:
691
- base64.b64decode(clean_b64)
692
- except Exception as e:
693
- logger.error(f"Invalid Base64 passed to model: {e}")
694
- raise
695
-
696
- # 6. Return with the correct data URI prefix
697
- return f"data:image/png;base64,{clean_b64}"
698
-
 
 
 
 
699
  def format_scratch_pseudo_code(code_string):
700
  """
701
  Parses and formats Scratch pseudo-code with correct indentation,
@@ -746,13 +791,13 @@ def format_scratch_pseudo_code(code_string):
746
 
747
  return '\n'.join(formatted_lines)
748
 
749
-
750
  # Node 1: Logic updating if any issue here
751
  def pseudo_generator_node(state: GameState):
752
  logger.info("--- Running plan_logic_aligner_node ---")
753
  image = state.get("project_image", "")
754
  project_json = state["project_json"]
755
-
 
756
  # MODIFICATION 1: Include 'Stage' in the list of names to plan for.
757
  # It's crucial to ensure 'Stage' is always present for its global role.
758
  target_names = [t["name"] for t in project_json["targets"]]
@@ -897,7 +942,7 @@ If you find any "Code-Blocks" then,
897
  "type": "image_url",
898
  "image_url": {
899
  # "url": f"data:image/png;base64,{image}"
900
- "url": clean_base64_for_model(image)
901
  }
902
  }
903
 
@@ -1852,6 +1897,7 @@ def overall_block_builder_node_2(state: GameState):
1852
 
1853
  # Node 6: variable adder node
1854
  def variable_adder_node(state: GameState):
 
1855
  project_json = state["project_json"]
1856
  try:
1857
  updated_project_json = variable_adder_main(project_json)
@@ -1861,239 +1907,23 @@ def variable_adder_node(state: GameState):
1861
  else:
1862
  print("Variable adder unable to add any variable inside the project!")
1863
  state["project_json"]=project_json
 
1864
  return state
1865
  except Exception as e:
1866
  logger.error(f"Error in variable adder node while updating project_json': {e}")
1867
  raise
1868
 
1869
-
1870
- # scratch_keywords = [
1871
- # "move", "turn", "wait", "repeat", "if", "else", "broadcast",
1872
- # "glide", "change", "forever", "when", "switch",
1873
- # "next costume", "set", "show", "hide", "play sound",
1874
- # "go to", "x position", "y position", "think", "say",
1875
- # "variable", "stop", "clone",
1876
- # "touching", "sensing", "pen", "clear","Scratch","Code","scratch blocks"
1877
- # ]
1878
-
1879
- # Node 6: Logic updating if any issue here
1880
- # def plan_logic_aligner_node(state: GameState):
1881
- # logger.info("--- Running plan_logic_aligner_node ---")
1882
-
1883
- # image = state.get("image", "")
1884
-
1885
- # refinement_prompt = f"""
1886
- # You are an expert in Scratch 3.0 game development, specializing in understanding block relationships (stacked, nested).
1887
- # "Analyze the Scratch code-block image and generate Pseudo-Code for what this logic appears to be doing."
1888
- # From Image, you also have to detect a value of Key given in Text form "Script for: ". Below is the example
1889
- # Example: "Script for: Bear", "Script for:" is a key and "Bear" is value.
1890
- # --- Scratch 3.0 Block Reference ---
1891
- # ### Hat Blocks
1892
- # Description: {hat_description}
1893
- # Blocks:
1894
- # {hat_opcodes_functionalities}
1895
-
1896
- # ### Boolean Blocks
1897
- # Description: {boolean_description}
1898
- # Blocks:
1899
- # {boolean_opcodes_functionalities}
1900
-
1901
- # ### C Blocks
1902
- # Description: {c_description}
1903
- # Blocks:
1904
- # {c_opcodes_functionalities}
1905
-
1906
- # ### Cap Blocks
1907
- # Description: {cap_description}
1908
- # Blocks:
1909
- # {cap_opcodes_functionalities}
1910
-
1911
- # ### Reporter Blocks
1912
- # Description: {reporter_description}
1913
- # Blocks:
1914
- # {reporter_opcodes_functionalities}
1915
-
1916
- # ### Stack Blocks
1917
- # Description: {stack_description}
1918
- # Blocks:
1919
- # {stack_opcodes_functionalities}
1920
- # -----------------------------------
1921
-
1922
- # Your task is to:
1923
- # If you don't find any "Code-Blocks" then,
1924
- # **Don't generate Pseudo Code, and pass the message "No Code-blocks" find...
1925
- # If you find any "Code-Blocks" then,
1926
- # 1. **Refine the 'logic'**: Make it precise, accurate, and fully aligned with the Game Description. Use Scratch‑consistent verbs and phrasing. **Do NOT** use raw double‑quotes inside the logic string.
1927
-
1928
- # 2. **Structural requirements**:
1929
- # - **Numeric values** `(e.g., 0, 5, 0.2, -130)` **must** be in parentheses: `(0)`, `(5)`, `(0.2)`, `(-130)`.
1930
- # - **AlphaNumeric values** `(e.g., hello, say 5, 4, hi!)` **must** be in parentheses: `(hello)`, `(say 5)`, `(4)`, `(hi!)`.
1931
- # - **Variables** must be in the form `[variable v]` (e.g., `[score v]`), even when used inside expressions two example use `set [score v] to (1)` or `show variable ([speed v])`.
1932
- # - **Dropdown options** must be in the form `[option v]` (e.g., `[Game Start v]`, `[blue sky v]`). example use `when [space v] key pressed`.
1933
- # - **Reporter blocks** used as inputs must be double‑wrapped: `((x position))`, `((y position))`. example use `if <((y position)) = (-130)> then` or `(((x position)) * (1))`.
1934
- # - **Boolean blocks** in conditions must be inside `< >`, including nested ones: `<not <condition>>`, `<<cond1> and <cond2>>`,`<<cond1> or <cond2>>`.
1935
- # - **Other Boolean blocks** in conditions must be inside `< >`, including nested ones or values or variables: `<(block/value/variable) * (block/value/variable)>`,`<(block/value/variable) < (block/value/variable)>`, and example of another variable`<[apple v] contains [a v]?>`.
1936
- # - **Operator expressions** must use explicit Scratch operator blocks, e.g.:
1937
- # ```
1938
- # (([ballSpeed v]) * (1.1))
1939
- # ```
1940
- # - **Every hat block script must end** with a final `end` on its own line.
1941
-
1942
- # 3. **Pseudo‑code formatting**:
1943
- # - Represent each block or nested block on its own line.
1944
- # - Indent nested blocks by 4 spaces under their parent (`forever`, `if`, etc.).
1945
- # - No comments or explanatory text—just the block sequence.
1946
- # - 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.
1947
-
1948
- # 4. **Logic content**:
1949
- # - Build clear flow for mechanics (movement, jumping, flying, scoring, collisions).
1950
- # - Match each action closely to a Scratch block or tight sequence.
1951
- # - Do **NOT** include any justification or comments—only the raw logic.
1952
-
1953
- # 5. **Examples for reference**:
1954
- # **Correct** pattern for a simple start script:
1955
- # ```
1956
- # when green flag clicked
1957
- # switch backdrop to [blue sky v]
1958
- # set [score v] to (0)
1959
- # show variable [score v]
1960
- # broadcast [Game Start v]
1961
- # end
1962
- # ```
1963
- # **Correct** pattern for updating the high score variable handling:
1964
- # ```
1965
- # when I receive [Game Over v]
1966
- # if <((score)) > (([High Score v]))> then
1967
- # set [High Score v] to ([score v])
1968
- # end
1969
- # switch backdrop to [Game Over v]
1970
- # end
1971
- # ```
1972
- # **Correct** pattern for level up and increase difficulty use:
1973
- # ```
1974
- # when I receive [Level Up v]
1975
- # change [level v] by (1)
1976
- # set [ballSpeed v] to ((([ballSpeed v]) * (1.1)))
1977
- # end
1978
- # ```
1979
- # **Correct** pattern for jumping mechanics use:
1980
- # ```
1981
- # when [space v] key pressed
1982
- # if <((y position)) = (-100)> then
1983
- # repeat (5)
1984
- # change y by (100)
1985
- # wait (0.1) seconds
1986
- # change y by (-100)
1987
- # wait (0.1) seconds
1988
- # end
1989
- # end
1990
- # end
1991
- # ```
1992
- # **Correct** pattern for continuos moving objects use:
1993
- # ```
1994
- # when green flag clicked
1995
- # go to x: (240) y: (-100)
1996
- # set [speed v] to (-5)
1997
- # show variable [speed v]
1998
- # forever
1999
- # change x by ([speed v])
2000
- # if <((x position)) < (-240)> then
2001
- # go to x: (240) y: (-100)
2002
- # end
2003
- # end
2004
- # end
2005
- # ```
2006
- # **Correct** pattern for continuos moving objects use:
2007
- # ```
2008
- # when green flag clicked
2009
- # go to x: (240) y: (-100)
2010
- # set [speed v] to (-5)
2011
- # show variable [speed v]
2012
- # forever
2013
- # change x by ([speed v])
2014
- # if <((x position)) < (-240)> then
2015
- # go to x: (240) y: (-100)
2016
- # end
2017
- # end
2018
- # end
2019
- # ```
2020
- # 6. **Donot** add any explaination of logic or comments to justify or explain just put the logic content in the json.
2021
- # 7. **Output**:
2022
- # Return **only** a JSON object, using double quotes everywhere:
2023
- # ```json
2024
- # {{
2025
- # "refined_logic":{{
2026
- # "name_variable": 'Value of "Sript for: "',
2027
- # "pseudocode":"…your fully‑formatted pseudo‑code here…",
2028
- # }}
2029
- # }}
2030
- # ```
2031
- # """
2032
- # image_input = {
2033
- # "type": "image_url",
2034
- # "image_url": {
2035
- # "url": f"data:image/png;base64,{image}"
2036
- # }
2037
- # }
2038
-
2039
- # content = [
2040
- # {"type": "text", "text": refinement_prompt},
2041
- # image_input
2042
- # ]
2043
-
2044
- # try:
2045
- # # Invoke the main agent for logic refinement and relationship identification
2046
- # response = agent.invoke({"messages": [{"role": "user", "content": content}]})
2047
- # llm_output_raw = response["messages"][-1].content.strip()
2048
-
2049
- # parsed_llm_output = extract_json_from_llm_response(llm_output_raw)
2050
-
2051
- # # result = parsed_llm_output
2052
- # # Extract needed values directly
2053
- # logic_data = parsed_llm_output.get("refined_logic", {})
2054
- # name_variable = logic_data.get("name_variable", "Unknown")
2055
- # pseudocode = logic_data.get("pseudocode", "No logic extracted")
2056
-
2057
- # result = {"pseudo_node": {
2058
- # "name_variable": name_variable,
2059
- # "pseudocode": pseudocode
2060
- # }}
2061
-
2062
- # print(f"result:\n\n {result}")
2063
- # return result
2064
- # except Exception as e:
2065
- # logger.error(f"❌ plan_logic_aligner_node failed: {str(e)}")
2066
- # return {"error": str(e)}
2067
- # except json.JSONDecodeError as error_json:
2068
- # # If JSON parsing fails, use the json resolver agent
2069
- # correction_prompt = (
2070
- # "Your task is to correct the provided JSON string to ensure it is **syntactically perfect and adheres strictly to JSON rules**.\n"
2071
- # "It must be a JSON object with `refined_logic` (string) and `block_relationships` (array of objects).\n"
2072
- # f"- **Error Details**: {error_json}\n\n"
2073
- # "**Strict Instructions for your response:**\n"
2074
- # "1. **ONLY** output the corrected JSON. Do not include any other text or explanations.\n"
2075
- # "2. Ensure all keys and string values are enclosed in **double quotes**. Escape internal quotes (`\\`).\n"
2076
- # "3. No trailing commas. Correct nesting.\n\n"
2077
- # "Here is the problematic JSON string to correct:\n"
2078
- # f"```json\n{llm_output_raw}\n```\n"
2079
- # "Corrected JSON:\n"
2080
- # )
2081
- # try:
2082
- # correction_response = agent_json_resolver.invoke({"messages": [{"role": "user", "content": correction_prompt}]})
2083
- # corrected_output = extract_json_from_llm_response(correction_response["messages"][-1].content)
2084
-
2085
- # result = {
2086
- # #"image_path": image_path,
2087
- # "pseudo_code": corrected_output
2088
- # }
2089
-
2090
- # return result
2091
-
2092
- # except Exception as e_corr:
2093
- # logger.error(f"Failed to correct JSON output for even after retry: {e_corr}")
2094
-
2095
- #def extract_images_from_pdf(pdf_path: Path, json_base_dir: Path, image_base_dir: Path):
2096
- #def extract_images_from_pdf(pdf_path: Path, json_base_dir: Path):
2097
 
2098
  # Prepare manipulated sprite JSON structure
2099
  manipulated_json = {}
@@ -2471,145 +2301,6 @@ def similarity_matching(sprites_data: str, project_folder: str) -> str:
2471
  json.dump(final_project, f, indent=2)
2472
 
2473
  return project_json_path
2474
- # for sprite_idx, matched_idx in enumerate(most_similar_indices):
2475
- # matched_image_path = folder_image_paths[matched_idx]
2476
- # matched_image_path = os.path.normpath(matched_image_path)
2477
- # print(" --------------------------------------1- matched_image_path ---------------------------------------",matched_image_path)
2478
- # matched_folder = os.path.dirname(matched_image_path)
2479
- # #folder_name = os.path.basename(matched_folder)
2480
- # print(" --------------------------------------1- matched_folder ---------------------------------------",matched_folder)
2481
- # if matched_folder in copied_folders:
2482
- # continue
2483
- # copied_folders.add(matched_folder)
2484
- # logger.info(f"Matched image path: {matched_image_path}")
2485
-
2486
- # sprite_json_path = os.path.join(matched_folder, 'sprite.json')
2487
- # print(" --------------------------------------- sprite_json_path ---------------------------------------",sprite_json_path)
2488
- # if not os.path.exists(sprite_json_path):
2489
- # logger.warning(f"sprite.json not found in: {matched_folder}")
2490
- # continue
2491
-
2492
- # with open(sprite_json_path, 'r') as f:
2493
- # sprite_data = json.load(f)
2494
- # # print(f"SPRITE DATA: \n{sprite_data}")
2495
- # # # Copy only non-matched files
2496
- # # for fname in os.listdir(matched_folder):
2497
- # # fpath = os.path.join(matched_folder, fname)
2498
- # # if os.path.isfile(fpath) and fname not in {os.path.basename(matched_image_path), 'sprite.json'}:
2499
- # # shutil.copy2(fpath, os.path.join(project_folder, fname))
2500
- # # # logger.info(f"Copied Sprite asset: {fname}")
2501
- # project_data.append(sprite_data)
2502
- # print(" --------------------------------------1- project_data ---------------------------------------",project_data)
2503
- # for fname in os.listdir(matched_folder):
2504
- # fpath = os.path.join(matched_folder, fname)
2505
- # dest_path = os.path.join(project_folder, fname)
2506
- # if os.path.isfile(fpath) and fname not in {os.path.basename(matched_image_path), 'sprite.json'}:
2507
- # shutil.copy2(fpath, dest_path)
2508
- # logger.info(f"🟢 Copied Sprite Asset: {fpath} → {dest_path}")
2509
-
2510
- # # ================================================================== #
2511
- # # Loop through most similar images from Backdrops folder #
2512
- # # → Copy Backdrop assets (excluding matched image + project.json) #
2513
- # # → Load project.json and append its data to project_data #
2514
- # # ================================================================== #
2515
- # backdrop_data = [] # for backdrop-related entries
2516
-
2517
- # for backdrop_idx, matched_idx in enumerate(most_similar_indices):
2518
- # matched_image_path = os.path.normpath(folder_image_paths[matched_idx])
2519
- # print(" --------------------------------------2- matched_image_path ---------------------------------------",matched_image_path)
2520
- # # Check if the match is from the Backdrops folder
2521
- # if matched_image_path.startswith(os.path.normpath(backdrop_images_path)):
2522
- # matched_folder = os.path.dirname(matched_image_path)
2523
- # print(" --------------------------------------2- matched_folder ---------------------------------------",matched_folder)
2524
- # folder_name = os.path.basename(matched_folder)
2525
-
2526
- # logger.info(f"Backdrop matched image: {matched_image_path}")
2527
-
2528
- # # Copy only non-matched files
2529
- # # for fname in os.listdir(matched_folder):
2530
- # # fpath = os.path.join(matched_folder, fname)
2531
- # # if os.path.isfile(fpath) and fname not in {os.path.basename(matched_image_path), 'project.json'}:
2532
- # # shutil.copy2(fpath, os.path.join(project_folder, fname))
2533
- # # # logger.info(f"Copied Backdrop asset: {fname}")
2534
- # for fname in os.listdir(matched_folder):
2535
- # fpath = os.path.join(matched_folder, fname)
2536
- # dest_path = os.path.join(project_folder, fname)
2537
- # if os.path.isfile(fpath) and fname not in {os.path.basename(matched_image_path), 'project.json'}:
2538
- # shutil.copy2(fpath, dest_path)
2539
- # logger.info(f"🟡 Copied Backdrop Asset: {fpath} → {dest_path}")
2540
-
2541
-
2542
- # # Append backdrop's project.json
2543
- # backdrop_json_path = os.path.join(matched_folder, 'project.json')
2544
- # print(" --------------------------------------2- backdrop_json_path ---------------------------------------",backdrop_json_path)
2545
- # if os.path.exists(backdrop_json_path):
2546
- # with open(backdrop_json_path, 'r') as f:
2547
- # backdrop_json_data = json.load(f)
2548
- # # print(f"SPRITE DATA: \n{backdrop_json_data}")
2549
- # if "targets" in backdrop_json_data:
2550
- # for target in backdrop_json_data["targets"]:
2551
- # if target.get("isStage") == True:
2552
- # backdrop_data.append(target)
2553
- # else:
2554
- # logger.warning(f"project.json not found in: {matched_folder}")
2555
-
2556
- # # Merge JSON structure
2557
- # final_project = {
2558
- # "targets": [],
2559
- # "monitors": [],
2560
- # "extensions": [],
2561
- # "meta": {
2562
- # "semver": "3.0.0",
2563
- # "vm": "11.3.0",
2564
- # "agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
2565
- # }
2566
- # }
2567
-
2568
- # for sprite in project_data:
2569
- # if not sprite.get("isStage", False):
2570
- # final_project["targets"].append(sprite)
2571
-
2572
- # if backdrop_data:
2573
- # all_costumes, sounds = [], []
2574
- # for idx, bd in enumerate(backdrop_data):
2575
- # all_costumes.extend(bd.get("costumes", []))
2576
- # if idx == 0 and "sounds" in bd:
2577
- # sounds = bd["sounds"]
2578
- # final_project["targets"].append({
2579
- # "isStage": True,
2580
- # "name": "Stage",
2581
- # "variables": {},
2582
- # "lists": {},
2583
- # "broadcasts": {},
2584
- # "blocks": {},
2585
- # "comments": {},
2586
- # "currentCostume": 1 if len(all_costumes) > 1 else 0,
2587
- # "costumes": all_costumes,
2588
- # "sounds": sounds,
2589
- # "volume": 100,
2590
- # "layerOrder": 0,
2591
- # "tempo": 60,
2592
- # "videoTransparency": 50,
2593
- # "videoState": "on",
2594
- # "textToSpeechLanguage": None
2595
- # })
2596
-
2597
- # with open(project_json_path, 'w') as f:
2598
- # json.dump(final_project, f, indent=2)
2599
-
2600
- # # logger.info(f"🎉 Final project saved: {project_json_path}")
2601
- # return project_json_path
2602
-
2603
- # def convert_bytes_to_image(pdf_bytes: bytes, dpi: int):
2604
- # images = convert_from_bytes(pdf_bytes, dpi=dpi, poppler_path=poppler_path)
2605
- # # Save each page to an in-memory BytesIO and return a list of BytesIOs
2606
- # buffers = []
2607
- # for img in images:
2608
- # buf = BytesIO()
2609
- # img.save(buf, format="PNG")
2610
- # buf.seek(0)
2611
- # buffers.append(buf)
2612
- # return buffers
2613
 
2614
  def convert_pdf_stream_to_images(pdf_stream: io.BytesIO, dpi=300):
2615
  # Ensure we are at the start of the stream
@@ -2625,7 +2316,7 @@ def convert_pdf_stream_to_images(pdf_stream: io.BytesIO, dpi=300):
2625
 
2626
  def delay_for_tpm_node(state: GameState):
2627
  logger.info("--- Running DelayForTPMNode ---")
2628
- time.sleep(1) # Adjust the delay as needed
2629
  logger.info("Delay completed.")
2630
  return state
2631
 
@@ -2645,18 +2336,37 @@ workflow.add_node("refined_planner", refined_planner_node) # Refines the action
2645
  workflow.add_node("opcode_counter", plan_opcode_counter_node)
2646
  workflow.add_node("block_builder", overall_block_builder_node_2)
2647
  workflow.add_node("variable_initializer", variable_adder_node)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2648
 
2649
- workflow.set_entry_point("pseudo_generator")
2650
- workflow.add_edge("pseudo_generator","time_delay_1")
2651
- workflow.add_edge("time_delay_1","plan_generator")
2652
- workflow.add_edge("plan_generator","time_delay_2")
2653
- # workflow.add_edge("time_delay_2",END)
2654
- workflow.add_edge("time_delay_2","refined_planner")
2655
- workflow.add_edge("refined_planner","time_delay_3")
2656
- workflow.add_edge("time_delay_3","opcode_counter")
2657
- workflow.add_edge("opcode_counter","block_builder")
2658
- workflow.add_edge("block_builder","variable_initializer")
2659
- workflow.add_edge("variable_initializer", END)
2660
  app_graph = workflow.compile()
2661
 
2662
  # ============== Helper function to Upscale an Image ============== #
@@ -2758,56 +2468,6 @@ def save_pdf_to_generated_dir(pdf_stream: io.BytesIO, project_id: str) -> str:
2758
  logger.error(f"Failed to save PDF to generated dir: {e}", exc_info=True)
2759
  return None
2760
 
2761
- # def pdf_to_images_with_size_check(pdf_path, output_dir, size_limit_mb=4):
2762
- # os.makedirs(output_dir, exist_ok=True)
2763
-
2764
- # # Convert PDF to images
2765
- # images = convert_from_path(pdf_path, dpi=300) # 300 DPI keeps quality
2766
- # saved_files = []
2767
-
2768
- # for i, img in enumerate(images, start=1):
2769
- # output_path = os.path.join(output_dir, f"page_{i}.jpg")
2770
-
2771
- # # Save to memory first to check size
2772
- # img_bytes = io.BytesIO()
2773
- # img.save(img_bytes, format="JPEG", quality=95) # near-lossless
2774
- # size_mb = len(img_bytes.getvalue()) / (1024 * 1024)
2775
-
2776
- # if size_mb > size_limit_mb:
2777
- # print(f"Page {i}: {size_mb:.2f} MB → compressing...")
2778
- # # Compress until under size limit
2779
- # quality = 95
2780
- # while size_mb > size_limit_mb and quality > 70: # don't go below 70
2781
- # img_bytes = io.BytesIO()
2782
- # img.save(img_bytes, format="JPEG", quality=quality)
2783
- # size_mb = len(img_bytes.getvalue()) / (1024 * 1024)
2784
- # quality -= 5
2785
- # else:
2786
- # print(f"Page {i}: {size_mb:.2f} MB → no compression needed.")
2787
-
2788
- # # Write final image to disk
2789
- # with open(output_path, "wb") as f:
2790
- # f.write(img_bytes.getvalue())
2791
-
2792
- # saved_files.append(output_path)
2793
-
2794
- # return saved_files
2795
- def compress_image_if_needed(image, max_size_mb=4, quality=85):
2796
- """
2797
- Compress the given PIL Image if its size is greater than max_size_mb.
2798
- Returns the (possibly compressed) image object.
2799
- """
2800
- temp_path = "/tmp/temp_compression_check.jpg"
2801
- image.save(temp_path, format="JPEG", quality=95) # save original temporarily
2802
- size_mb = os.path.getsize(temp_path) / (1024 * 1024)
2803
-
2804
- if size_mb > max_size_mb:
2805
- # Compress by reducing quality
2806
- image.save(temp_path, format="JPEG", quality=quality, optimize=True)
2807
- print(f"Image compressed from {size_mb:.2f} MB to {os.path.getsize(temp_path)/(1024*1024):.2f} MB")
2808
- return Image.open(temp_path)
2809
- return image
2810
-
2811
  @app.route('/')
2812
  def index():
2813
  return render_template('app_index.html')
@@ -2934,12 +2594,6 @@ def process_pdf():
2934
  images = convert_pdf_stream_to_images(pdf_stream, dpi=300)
2935
  else:
2936
  images = convert_from_path(pdf_stream, dpi=300)
2937
-
2938
- # Compress images if needed
2939
- compressed_images = []
2940
- for img in images:
2941
- compressed_images.append(compress_image_if_needed(img, max_size_mb=4, quality=85))
2942
- images = compressed_images
2943
 
2944
  #updating logic here [Dev Patel]
2945
  initial_state_dict = {
@@ -2951,6 +2605,10 @@ def process_pdf():
2951
  "action_plan": {},
2952
  "pseudo_code": {},
2953
  "temporary_node": {},
 
 
 
 
2954
  }
2955
 
2956
  final_state_dict = app_graph.invoke(initial_state_dict) # Pass dictionary
 
141
  # JSON_DIR,
142
  ):
143
  d.mkdir(parents=True, exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
+ # class GameState(TypedDict):
146
+ # project_json: dict
147
+ # description: str
148
+ # project_id: str
149
+ # project_image: str
150
+ # pseudo_code: dict
151
+ # action_plan: Optional[Dict]
152
+ # temporary_node: Optional[Dict]
153
+
154
  class GameState(TypedDict):
155
  project_json: dict
156
  description: str
 
159
  pseudo_code: dict
160
  action_plan: Optional[Dict]
161
  temporary_node: Optional[Dict]
162
+ page_count: int
163
+ processing: bool
164
+ temp_pseudo_code: list
 
165
 
166
  # Refined SYSTEM_PROMPT with more explicit Scratch JSON rules, especially for variables
167
  SYSTEM_PROMPT = """
 
266
  prompt=SYSTEM_PROMPT_JSON_CORRECTOR
267
  )
268
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  # Helper function to load the block catalog from a JSON file
270
  def _load_block_catalog(block_type: str) -> Dict:
271
  """
 
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
  """
746
  Parses and formats Scratch pseudo-code with correct indentation,
 
791
 
792
  return '\n'.join(formatted_lines)
793
 
 
794
  # Node 1: Logic updating if any issue here
795
  def pseudo_generator_node(state: GameState):
796
  logger.info("--- Running plan_logic_aligner_node ---")
797
  image = state.get("project_image", "")
798
  project_json = state["project_json"]
799
+ cnt =state["page_count"]
800
+ print(f"The page number recived at the pseudo_generator node:-----> {cnt}")
801
  # MODIFICATION 1: Include 'Stage' in the list of names to plan for.
802
  # It's crucial to ensure 'Stage' is always present for its global role.
803
  target_names = [t["name"] for t in project_json["targets"]]
 
942
  "type": "image_url",
943
  "image_url": {
944
  # "url": f"data:image/png;base64,{image}"
945
+ "url": clean_base64_for_model(image[cnt])
946
  }
947
  }
948
 
 
1897
 
1898
  # Node 6: variable adder node
1899
  def variable_adder_node(state: GameState):
1900
+ logger.info("--- Running Variable Adder Node ---")
1901
  project_json = state["project_json"]
1902
  try:
1903
  updated_project_json = variable_adder_main(project_json)
 
1907
  else:
1908
  print("Variable adder unable to add any variable inside the project!")
1909
  state["project_json"]=project_json
1910
+ state["page_count"] +=1
1911
  return state
1912
  except Exception as e:
1913
  logger.error(f"Error in variable adder node while updating project_json': {e}")
1914
  raise
1915
 
1916
+ # Node 7: variable adder node
1917
+ def processed_page_node(state: GameState):
1918
+ logger.info("--- Processing the Pages Node ---")
1919
+ image = state.get("project_image", "")
1920
+ cnt =state["page_count"]
1921
+ print(f"The page processed for page:--------------> {cnt}")
1922
+ if cnt<len(image):
1923
+ state["processing"]= True
1924
+ else:
1925
+ state["processing"]= False
1926
+ return state
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1927
 
1928
  # Prepare manipulated sprite JSON structure
1929
  manipulated_json = {}
 
2301
  json.dump(final_project, f, indent=2)
2302
 
2303
  return project_json_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2304
 
2305
  def convert_pdf_stream_to_images(pdf_stream: io.BytesIO, dpi=300):
2306
  # Ensure we are at the start of the stream
 
2316
 
2317
  def delay_for_tpm_node(state: GameState):
2318
  logger.info("--- Running DelayForTPMNode ---")
2319
+ time.sleep(10) # Adjust the delay as needed
2320
  logger.info("Delay completed.")
2321
  return state
2322
 
 
2336
  workflow.add_node("opcode_counter", plan_opcode_counter_node)
2337
  workflow.add_node("block_builder", overall_block_builder_node_2)
2338
  workflow.add_node("variable_initializer", variable_adder_node)
2339
+ workflow.add_node("page_processed", processed_page_node)
2340
+
2341
+ workflow.set_entry_point("page_processed")
2342
+ # Conditional branching from the start
2343
+ def decide_next_step(state: GameState):
2344
+ if state.get("processing", False):
2345
+ return "pseudo_generator"
2346
+ else:
2347
+ return END
2348
+
2349
+ workflow.add_conditional_edges(
2350
+ "page_processed",
2351
+ decide_next_step,
2352
+ {
2353
+ "pseudo_generator": "pseudo_generator",
2354
+ "END": END
2355
+ }
2356
+ )
2357
+ # Main chain
2358
+ workflow.add_edge("pseudo_generator", "time_delay_1")
2359
+ workflow.add_edge("time_delay_1", "plan_generator")
2360
+ workflow.add_edge("plan_generator", "time_delay_2")
2361
+ workflow.add_edge("time_delay_2", "refined_planner")
2362
+ workflow.add_edge("refined_planner", "time_delay_3")
2363
+ workflow.add_edge("time_delay_3", "opcode_counter")
2364
+ workflow.add_edge("opcode_counter", "block_builder")
2365
+ workflow.add_edge("block_builder", "variable_initializer")
2366
+
2367
+ # After last node, check again
2368
+ workflow.add_edge("variable_initializer", "page_processed")
2369
 
 
 
 
 
 
 
 
 
 
 
 
2370
  app_graph = workflow.compile()
2371
 
2372
  # ============== Helper function to Upscale an Image ============== #
 
2468
  logger.error(f"Failed to save PDF to generated dir: {e}", exc_info=True)
2469
  return None
2470
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2471
  @app.route('/')
2472
  def index():
2473
  return render_template('app_index.html')
 
2594
  images = convert_pdf_stream_to_images(pdf_stream, dpi=300)
2595
  else:
2596
  images = convert_from_path(pdf_stream, dpi=300)
 
 
 
 
 
 
2597
 
2598
  #updating logic here [Dev Patel]
2599
  initial_state_dict = {
 
2605
  "action_plan": {},
2606
  "pseudo_code": {},
2607
  "temporary_node": {},
2608
+ "temporary_node": {},
2609
+ "processing":True,
2610
+ "page_count": 0,
2611
+ "temp_pseudo_code":{},
2612
  }
2613
 
2614
  final_state_dict = app_graph.invoke(initial_state_dict) # Pass dictionary