prthm11 commited on
Commit
33868ff
·
verified ·
1 Parent(s): 0d2ede1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +157 -73
app.py CHANGED
@@ -624,123 +624,207 @@ def extract_json_from_llm_response(raw_response: str) -> dict:
624
  logger.error("Sanitized JSON still invalid:\n%s", json_string)
625
  raise
626
 
627
- # def clean_base64_for_model(raw_b64):
628
  # """
629
- # Normalize input into a valid data:image/png;base64,<payload> string.
630
-
631
- # Accepts:
632
- # - a list of base64 strings → picks the first element
633
- # - a PIL Image instance → encodes to PNG/base64
634
- # - a raw base64 string → strips whitespace and data URI prefix
635
  # """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
636
  # if not raw_b64:
637
- # return ""
638
 
639
- # # 1. If it’s a list, take its first element
640
  # if isinstance(raw_b64, list):
641
  # raw_b64 = raw_b64[0] if raw_b64 else ""
642
  # if not raw_b64:
643
- # return ""
644
 
645
- # # 2. If it’s a PIL Image, convert to base64
646
  # if isinstance(raw_b64, Image.Image):
647
  # buf = io.BytesIO()
648
  # raw_b64.save(buf, format="PNG")
649
  # raw_b64 = base64.b64encode(buf.getvalue()).decode()
650
 
651
- # # 3. At this point it must be a string
652
  # if not isinstance(raw_b64, str):
653
  # raise TypeError(f"Expected base64 string or PIL Image, got {type(raw_b64)}")
654
 
655
- # # 4. Strip any existing data URI prefix, whitespace, or newlines
656
  # clean_b64 = re.sub(r"^data:image\/[a-zA-Z]+;base64,", "", raw_b64)
657
  # clean_b64 = clean_b64.replace("\n", "").replace("\r", "").strip()
658
 
659
- # # 5. Validate it’s proper base64
660
- # try:
661
- # base64.b64decode(clean_b64)
662
- # except Exception as e:
663
- # logger.error(f"Invalid Base64 passed to model: {e}")
664
- # raise
665
-
666
- # # 6. Return with the correct data URI prefix
667
- # return f"data:image/png;base64,{clean_b64}"
668
-
669
- def reduce_image_size_to_limit(clean_b64_str, max_kb=4000):
 
 
 
 
670
  """
671
- Reduce an image's size to be as close as possible to max_kb without exceeding it.
672
- Returns the final base64 string and its size in KB.
 
 
673
  """
674
- import re, base64
675
- from io import BytesIO
676
- from PIL import Image
 
 
 
677
 
678
- # Remove the data URI prefix
679
- base64_data = re.sub(r"^data:image\/[a-zA-Z]+;base64,", "", clean_b64_str)
680
- image_data = base64.b64decode(base64_data)
681
-
682
- # Load into PIL
683
- img = Image.open(BytesIO(image_data))
684
-
685
- low, high = 20, 95 # reasonable JPEG quality range
686
- best_b64 = None
687
- best_size_kb = 0
688
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
689
  while low <= high:
690
  mid = (low + high) // 2
691
- buffer = BytesIO()
692
- img.save(buffer, format="JPEG", quality=mid)
693
- size_kb = len(buffer.getvalue()) / 1024
694
-
 
 
 
 
695
  if size_kb <= max_kb:
696
- # This quality is valid, try higher
697
- best_b64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
698
- best_size_kb = size_kb
699
  low = mid + 1
700
  else:
701
- # Too big, try lower
702
  high = mid - 1
703
 
704
- return f"data:image/jpeg;base64,{best_b64}"
 
 
 
 
 
 
 
 
705
 
706
- #clean the base64 model here
707
- def clean_base64_for_model(raw_b64):
708
- import io, base64, re
709
- from PIL import Image
710
 
 
 
 
 
 
 
 
 
 
 
 
711
  if not raw_b64:
712
- return "", ""
713
 
714
  if isinstance(raw_b64, list):
715
  raw_b64 = raw_b64[0] if raw_b64 else ""
716
  if not raw_b64:
717
- return "", ""
718
 
719
  if isinstance(raw_b64, Image.Image):
720
  buf = io.BytesIO()
721
- raw_b64.save(buf, format="PNG")
722
- raw_b64 = base64.b64encode(buf.getvalue()).decode()
 
 
 
 
723
 
724
  if not isinstance(raw_b64, str):
725
  raise TypeError(f"Expected base64 string or PIL Image, got {type(raw_b64)}")
726
 
727
- # Remove data URI prefix if present
728
- clean_b64 = re.sub(r"^data:image\/[a-zA-Z]+;base64,", "", raw_b64)
729
- clean_b64 = clean_b64.replace("\n", "").replace("\r", "").strip()
730
-
731
- # Log original size
732
- original_size = len(clean_b64.encode("utf-8"))
733
- print(f"Original Base64 size (bytes): {original_size}")
734
- if original_size > 4000000:
735
- # Reduce size to under 4 MB
736
- reduced_b64 = reduce_image_size_to_limit(clean_b64, max_kb=4000)
737
- clean_b64_2 = re.sub(r"^data:image\/[a-zA-Z]+;base64,", "", reduced_b64)
738
- clean_b64_2 = clean_b64_2.replace("\n", "").replace("\r", "").strip()
739
- reduced_size = len(clean_b64_2.encode("utf-8"))
740
- print(f"Reduced Base64 size (bytes): {reduced_size}")
741
- # Return both prefixed and clean reduced versions
742
- return f"data:image/jpeg;base64,{reduced_b64}"
743
- return f"data:image/jpeg;base64,{clean_b64}"
 
 
 
 
 
 
 
 
 
 
 
 
 
744
 
745
  SCRATCH_OPCODES = [
746
  'motion_movesteps', 'motion_turnright', 'motion_turnleft', 'motion_goto',
 
624
  logger.error("Sanitized JSON still invalid:\n%s", json_string)
625
  raise
626
 
627
+ # def reduce_image_size_to_limit(clean_b64_str, max_kb=4000):
628
  # """
629
+ # Reduce an image's size to be as close as possible to max_kb without exceeding it.
630
+ # Returns the final base64 string and its size in KB.
 
 
 
 
631
  # """
632
+ # import re, base64
633
+ # from io import BytesIO
634
+ # from PIL import Image
635
+
636
+ # # Remove the data URI prefix
637
+ # base64_data = re.sub(r"^data:image\/[a-zA-Z]+;base64,", "", clean_b64_str)
638
+ # image_data = base64.b64decode(base64_data)
639
+
640
+ # # Load into PIL
641
+ # img = Image.open(BytesIO(image_data))
642
+
643
+ # low, high = 20, 95 # reasonable JPEG quality range
644
+ # best_b64 = None
645
+ # best_size_kb = 0
646
+
647
+ # while low <= high:
648
+ # mid = (low + high) // 2
649
+ # buffer = BytesIO()
650
+ # img.save(buffer, format="JPEG", quality=mid)
651
+ # size_kb = len(buffer.getvalue()) / 1024
652
+
653
+ # if size_kb <= max_kb:
654
+ # # This quality is valid, try higher
655
+ # best_b64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
656
+ # best_size_kb = size_kb
657
+ # low = mid + 1
658
+ # else:
659
+ # # Too big, try lower
660
+ # high = mid - 1
661
+
662
+ # return f"data:image/jpeg;base64,{best_b64}"
663
+
664
+ # #clean the base64 model here
665
+ # def clean_base64_for_model(raw_b64):
666
+ # import io, base64, re
667
+ # from PIL import Image
668
+
669
  # if not raw_b64:
670
+ # return "", ""
671
 
 
672
  # if isinstance(raw_b64, list):
673
  # raw_b64 = raw_b64[0] if raw_b64 else ""
674
  # if not raw_b64:
675
+ # return "", ""
676
 
 
677
  # if isinstance(raw_b64, Image.Image):
678
  # buf = io.BytesIO()
679
  # raw_b64.save(buf, format="PNG")
680
  # raw_b64 = base64.b64encode(buf.getvalue()).decode()
681
 
 
682
  # if not isinstance(raw_b64, str):
683
  # raise TypeError(f"Expected base64 string or PIL Image, got {type(raw_b64)}")
684
 
685
+ # # Remove data URI prefix if present
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
+ # # Log original size
690
+ # original_size = len(clean_b64.encode("utf-8"))
691
+ # print(f"Original Base64 size (bytes): {original_size}")
692
+ # if original_size > 4000000:
693
+ # # Reduce size to under 4 MB
694
+ # reduced_b64 = reduce_image_size_to_limit(clean_b64, max_kb=4000)
695
+ # clean_b64_2 = re.sub(r"^data:image\/[a-zA-Z]+;base64,", "", reduced_b64)
696
+ # clean_b64_2 = clean_b64_2.replace("\n", "").replace("\r", "").strip()
697
+ # reduced_size = len(clean_b64_2.encode("utf-8"))
698
+ # print(f"Reduced Base64 size (bytes): {reduced_size}")
699
+ # # Return both prefixed and clean reduced versions
700
+ # return f"data:image/jpeg;base64,{reduced_b64}"
701
+ # return f"data:image/jpeg;base64,{clean_b64}"
702
+
703
+ def reduce_image_size_to_limit(clean_b64_str: str, max_kb: int = 4000) -> str:
704
  """
705
+ Input: clean_b64_str = BASE64 STRING (no data: prefix)
706
+ Output: BASE64 STRING (no data: prefix), sized as close as possible to max_kb KB.
707
+ Guarantees: returns a valid base64 string (never None). May still be larger than max_kb
708
+ if saving at lowest quality cannot get under the limit.
709
  """
710
+ # sanitize
711
+ clean = re.sub(r"\s+", "", clean_b64_str).strip()
712
+ # fix padding
713
+ missing = len(clean) % 4
714
+ if missing:
715
+ clean += "=" * (4 - missing)
716
 
717
+ try:
718
+ image_data = base64.b64decode(clean)
719
+ except Exception as e:
720
+ raise ValueError("Invalid base64 input to reduce_image_size_to_limit") from e
 
 
 
 
 
 
721
 
722
+ try:
723
+ img = Image.open(io.BytesIO(image_data))
724
+ img.load()
725
+ except Exception as e:
726
+ raise ValueError("Could not open image from base64") from e
727
+
728
+ # convert alpha -> RGB because JPEG doesn't support alpha
729
+ if img.mode in ("RGBA", "LA") or (img.mode == "P" and "transparency" in img.info):
730
+ background = Image.new("RGB", img.size, (255, 255, 255))
731
+ background.paste(img, mask=img.split()[-1] if img.mode != "RGB" else None)
732
+ img = background
733
+ elif img.mode != "RGB":
734
+ img = img.convert("RGB")
735
+
736
+ low, high = 20, 95
737
+ best_bytes = None
738
+ # binary search for best quality
739
  while low <= high:
740
  mid = (low + high) // 2
741
+ buf = io.BytesIO()
742
+ try:
743
+ img.save(buf, format="JPEG", quality=mid, optimize=True)
744
+ except OSError:
745
+ # some PIL builds/channels may throw on optimize=True; fallback without optimize
746
+ buf = io.BytesIO()
747
+ img.save(buf, format="JPEG", quality=mid)
748
+ size_kb = len(buf.getvalue()) / 1024.0
749
  if size_kb <= max_kb:
750
+ best_bytes = buf.getvalue()
 
 
751
  low = mid + 1
752
  else:
 
753
  high = mid - 1
754
 
755
+ # if never found a quality <= max_kb, use the smallest we created (quality = 20)
756
+ if best_bytes is None:
757
+ buf = io.BytesIO()
758
+ try:
759
+ img.save(buf, format="JPEG", quality=20, optimize=True)
760
+ except OSError:
761
+ buf = io.BytesIO()
762
+ img.save(buf, format="JPEG", quality=20)
763
+ best_bytes = buf.getvalue()
764
 
765
+ return base64.b64encode(best_bytes).decode("utf-8")
 
 
 
766
 
767
+
768
+ def clean_base64_for_model(raw_b64, max_bytes_threshold=4000000) -> str:
769
+ """
770
+ Accepts: raw_b64 can be:
771
+ - a data URI 'data:image/png;base64,...'
772
+ - a plain base64 string
773
+ - a PIL Image
774
+ - a list containing the above (take first)
775
+ Returns: a data URI string 'data:<mime>;base64,<base64>' guaranteed to be syntactically valid.
776
+ """
777
+ # normalize input
778
  if not raw_b64:
779
+ return ""
780
 
781
  if isinstance(raw_b64, list):
782
  raw_b64 = raw_b64[0] if raw_b64 else ""
783
  if not raw_b64:
784
+ return ""
785
 
786
  if isinstance(raw_b64, Image.Image):
787
  buf = io.BytesIO()
788
+ # convert to RGB and save as JPEG to keep consistent
789
+ img = raw_b64.convert("RGB")
790
+ img.save(buf, format="JPEG")
791
+ clean_b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
792
+ mime = "image/jpeg"
793
+ return f"data:{mime};base64,{clean_b64}"
794
 
795
  if not isinstance(raw_b64, str):
796
  raise TypeError(f"Expected base64 string or PIL Image, got {type(raw_b64)}")
797
 
798
+ # detect mime if present; otherwise default to png
799
+ m = re.match(r"^data:(image\/[a-zA-Z0-9.+-]+);base64,(.+)$", raw_b64, flags=re.DOTALL)
800
+ if m:
801
+ mime = m.group(1)
802
+ clean_b64 = m.group(2)
803
+ else:
804
+ # no prefix; assume png by default (you can change to jpeg if you prefer)
805
+ mime = "image/png"
806
+ clean_b64 = raw_b64
807
+
808
+ # sanitize base64 string
809
+ clean_b64 = re.sub(r"\s+", "", clean_b64).strip()
810
+ missing = len(clean_b64) % 4
811
+ if missing:
812
+ clean_b64 += "=" * (4 - missing)
813
+
814
+ original_size_bytes = len(clean_b64.encode("utf-8"))
815
+ # debug print
816
+ print(f"Original base64 size (bytes): {original_size_bytes}, mime: {mime}")
817
+
818
+ if original_size_bytes > max_bytes_threshold:
819
+ # reduce and return JPEG prefixed data URI (JPEG tends to compress better for photos)
820
+ reduced_clean = reduce_image_size_to_limit(clean_b64, max_kb=4000)
821
+ # reduced_clean is plain base64 (no prefix)
822
+ print(f"Reduced base64 size (bytes): {original_size_bytes}, mime: {mime}")
823
+ return f"data:image/jpeg;base64,{reduced_clean}"
824
+
825
+ # otherwise return original with its mime prefix (ensure prefix exists)
826
+ return f"data:{mime};base64,{clean_b64}"
827
+
828
 
829
  SCRATCH_OPCODES = [
830
  'motion_movesteps', 'motion_turnright', 'motion_turnleft', 'motion_goto',