rairo commited on
Commit
b899d5b
·
verified ·
1 Parent(s): 367e4d8

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +85 -117
main.py CHANGED
@@ -568,148 +568,116 @@ def create_project():
568
  def approve_project_plan(project_id):
569
  uid = verify_token(request.headers.get('Authorization'))
570
  if not uid: return jsonify({'error': 'Unauthorized'}), 401
571
-
572
  project_ref = db_ref.child(f'projects/{project_id}')
573
  project_data = project_ref.get()
574
  if not project_data or project_data.get('uid') != uid:
575
  return jsonify({'error': 'Project not found or access denied'}), 404
576
 
577
  selected_option = request.json.get('selectedOption')
578
-
579
- # Optimized image download with timeout and session reuse
580
- try:
581
- session = requests.Session()
582
- session.headers.update({'User-Agent': 'Mozilla/5.0'})
583
-
584
- with session.get(project_data['userImageURL'], timeout=15, stream=True) as response:
585
- response.raise_for_status()
586
- image_data = io.BytesIO()
587
- for chunk in response.iter_content(chunk_size=8192):
588
- image_data.write(chunk)
589
- image_data.seek(0)
590
- pil_image = Image.open(image_data)
591
-
592
- except Exception as e:
593
- return jsonify({'error': f'Failed to download image: {str(e)}'}), 400
594
-
595
- context = f"The user chose the upcycling project: '{selected_option}'." if selected_option else f"The user has approved the plan for '{project_data['projectTitle']}'."
596
-
597
- # Streamlined prompt
598
- detailed_prompt = f"""You are a DIY expert. The user wants to proceed with the project titled "{project_data['projectTitle']}".
599
- {context}
600
- Provide a detailed guide. For each step, you MUST provide a simple, clear illustrative image.
601
- Format your response EXACTLY like this:
602
- TOOLS AND MATERIALS:
603
- - Tool A
604
- - Material B
605
- STEPS(Maximum 7 steps):
606
- 1. First step instructions.
607
- 2. Second step instructions..."""
608
 
609
  try:
610
- # AI generation with retry logic
611
- max_retries = 2
612
- full_resp = None
613
-
614
- for attempt in range(max_retries):
615
- try:
616
- chat = client.chats.create(model=GENERATION_MODEL, config=types.GenerateContentConfig(response_modalities=["Text", "Image"]))
617
- full_resp = chat.send_message([detailed_prompt, pil_image])
618
- break
619
- except Exception as e:
620
- if attempt == max_retries - 1:
621
- raise e
622
- print(f"AI generation attempt {attempt + 1} failed: {e}")
623
-
624
- # Optimized response parsing
625
  gen_parts = full_resp.candidates[0].content.parts
626
-
627
  combined_text = ""
628
  inline_images = []
629
-
630
- # Single pass through parts
631
  for part in gen_parts:
632
- if hasattr(part, 'text') and part.text:
633
  combined_text += part.text + "\n"
634
- if hasattr(part, 'inline_data') and part.inline_data:
635
- try:
636
- img = Image.open(BytesIO(part.inline_data.data))
637
- inline_images.append(img)
638
- except Exception as e:
639
- print(f"Failed to process image: {e}")
640
- continue
641
-
642
  combined_text = combined_text.strip()
643
-
644
- # More robust regex parsing
645
- tools_match = re.search(r"TOOLS AND MATERIALS:\s*(.*?)\s*(?=STEPS:|$)", combined_text, re.DOTALL | re.IGNORECASE)
646
- steps_match = re.search(r"STEPS[^:]*:\s*(.*)", combined_text, re.DOTALL | re.IGNORECASE)
647
-
648
- if not tools_match or not steps_match:
649
- return jsonify({'error': 'AI response format error - could not parse tools and steps'}), 500
650
-
651
- tools_section = tools_match.group(1).strip()
652
- steps_section = steps_match.group(1).strip()
653
-
654
- # Efficient list comprehension
655
- tools_list = [line.strip().lstrip('- ').strip() for line in tools_section.split('\n') if line.strip()]
656
  parsed_steps = parse_numbered_steps(steps_section)
657
 
658
- if len(parsed_steps) != len(inline_images):
659
- return jsonify({'error': f'AI response mismatch: {len(parsed_steps)} steps vs {len(inline_images)} images'}), 500
660
 
661
- # Sequential processing with progress tracking
662
  final_steps = []
663
- session = requests.Session() # Reuse session for uploads
664
-
665
- for i, (step_info, image) in enumerate(zip(parsed_steps, inline_images)):
666
- try:
667
- # Optimize image before upload
668
- if image.mode == 'RGBA':
669
- image = image.convert('RGB')
670
-
671
- # Compress image to reduce upload time
672
- img_byte_arr = io.BytesIO()
673
- image.save(img_byte_arr, format='JPEG', quality=85, optimize=True)
674
- img_path = f"users/{uid}/projects/{project_id}/steps/step_{i+1}_image.jpg"
675
-
676
- # Upload with timeout
677
- img_url = upload_to_storage(img_byte_arr.getvalue(), img_path, 'image/jpeg')
678
-
679
- # Generate audio with timeout handling
680
- try:
681
- narration_url = generate_tts_audio_and_upload(step_info['text'], uid, project_id, i + 1)
682
- except Exception as audio_error:
683
- print(f"Audio generation failed for step {i+1}: {audio_error}")
684
- narration_url = "" # Continue without audio if it fails
685
-
686
- step_info.update({
687
- "imageUrl": img_url,
688
- "narrationUrl": narration_url,
689
- "isDone": False,
690
- "notes": ""
691
- })
692
- final_steps.append(step_info)
693
-
694
- except Exception as step_error:
695
- print(f"Failed to process step {i+1}: {step_error}")
696
- return jsonify({'error': f'Failed to process step {i+1}'}), 500
697
-
698
- # Single database update
699
  update_data = {
700
- "status": "ready",
701
- "toolsList": tools_list,
702
- "steps": final_steps,
703
  "selectedOption": selected_option or ""
704
  }
705
-
706
  project_ref.update(update_data)
707
-
708
  return jsonify({"success": True, **update_data})
709
 
710
  except Exception as e:
711
- print(f"Error in approve_project_plan: {traceback.format_exc()}")
712
- return jsonify({'error': f"Failed to generate detailed guide: {str(e)}"}), 500
713
 
714
  @app.route('/api/projects', methods=['GET'])
715
  def list_projects():
 
568
  def approve_project_plan(project_id):
569
  uid = verify_token(request.headers.get('Authorization'))
570
  if not uid: return jsonify({'error': 'Unauthorized'}), 401
571
+
572
  project_ref = db_ref.child(f'projects/{project_id}')
573
  project_data = project_ref.get()
574
  if not project_data or project_data.get('uid') != uid:
575
  return jsonify({'error': 'Project not found or access denied'}), 404
576
 
577
  selected_option = request.json.get('selectedOption')
578
+
579
+ # 1) Download & compress the user’s image before sending it off to Gemini
580
+ response = requests.get(project_data['userImageURL'])
581
+ pil_image = Image.open(io.BytesIO(response.content)).convert("RGB")
582
+ pil_image.thumbnail((1024, 1024)) # max‐side 1024px
583
+ buf = io.BytesIO()
584
+ pil_image.save(buf, format='JPEG', quality=75, optimize=True)
585
+ buf.seek(0)
586
+ compressed_image = Image.open(buf)
587
+
588
+ context = (f"The user chose the upcycling project: '{selected_option}'."
589
+ if selected_option
590
+ else f"The user has approved the plan for '{project_data['projectTitle']}'.")
591
+
592
+ detailed_prompt = f"""
593
+ You are a DIY expert. The user wants to proceed with the project titled "{project_data['projectTitle']}".
594
+ {context}
595
+ Provide a detailed guide. For each step, you MUST provide a simple, clear illustrative image.
596
+ Format your response EXACTLY like this:
597
+
598
+ TOOLS AND MATERIALS:
599
+ - Tool A
600
+ - Material B
601
+
602
+ STEPS(Maximum 7 steps):
603
+ 1. First step instructions.
604
+ 2. Second step instructions...
605
+ """
 
 
606
 
607
  try:
608
+ chat = client.chats.create(
609
+ model=GENERATION_MODEL,
610
+ config=types.GenerateContentConfig(response_modalities=["Text", "Image"])
611
+ )
612
+ full_resp = chat.send_message([detailed_prompt, compressed_image])
613
+
 
 
 
 
 
 
 
 
 
614
  gen_parts = full_resp.candidates[0].content.parts
 
615
  combined_text = ""
616
  inline_images = []
617
+
 
618
  for part in gen_parts:
619
+ if part.text:
620
  combined_text += part.text + "\n"
621
+ if part.inline_data:
622
+ img = Image.open(BytesIO(part.inline_data.data)).convert("RGB")
623
+ # 2) Immediately downscale each AI‐generated image
624
+ img.thumbnail((800, 800))
625
+ inline_images.append(img)
 
 
 
626
  combined_text = combined_text.strip()
627
+
628
+ # parse out tools + steps
629
+ tools_section = re.search(r"TOOLS AND MATERIALS:\s*(.*?)\s*STEPS:", combined_text, re.DOTALL).group(1).strip()
630
+ steps_section = re.search(r"STEPS:\s*(.*)", combined_text, re.DOTALL).group(1).strip()
631
+ tools_list = [line.strip('- ').strip() for line in tools_section.splitlines() if line.strip()]
 
 
 
 
 
 
 
 
632
  parsed_steps = parse_numbered_steps(steps_section)
633
 
634
+ if len(parsed_steps) != len(inline_images):
635
+ return jsonify({'error': 'AI response mismatch: Steps and images do not match.'}), 500
636
 
 
637
  final_steps = []
638
+ for i, step_info in enumerate(parsed_steps):
639
+ # 3) Save JPEG at 70% quality
640
+ img_byte_arr = io.BytesIO()
641
+ inline_images[i].save(
642
+ img_byte_arr,
643
+ format='JPEG',
644
+ quality=70,
645
+ optimize=True
646
+ )
647
+ img_path = f"users/{uid}/projects/{project_id}/steps/step_{i+1}_image.jpg"
648
+ img_url = upload_to_storage(img_byte_arr.getvalue(), img_path, 'image/jpeg')
649
+
650
+ # 4) Generate and compress TTS audio to mp3 @ 64kbps
651
+ raw_audio = generate_tts_audio(step_info['text']) # returns raw bytes
652
+ from pydub import AudioSegment
653
+ sound = AudioSegment.from_file(io.BytesIO(raw_audio), format="wav")
654
+ mp3_buf = io.BytesIO()
655
+ sound.export(mp3_buf, format="mp3", bitrate="64k")
656
+ mp3_buf.seek(0)
657
+ narration_url = upload_to_storage(mp3_buf.read(),
658
+ f"users/{uid}/projects/{project_id}/steps/step_{i+1}_tts.mp3",
659
+ 'audio/mpeg')
660
+
661
+ step_info.update({
662
+ "imageUrl": img_url,
663
+ "narrationUrl": narration_url,
664
+ "isDone": False,
665
+ "notes": ""
666
+ })
667
+ final_steps.append(step_info)
668
+
 
 
 
 
 
669
  update_data = {
670
+ "status": "ready",
671
+ "toolsList": tools_list,
672
+ "steps": final_steps,
673
  "selectedOption": selected_option or ""
674
  }
 
675
  project_ref.update(update_data)
 
676
  return jsonify({"success": True, **update_data})
677
 
678
  except Exception as e:
679
+ print(traceback.format_exc())
680
+ return jsonify({'error': f"Failed to generate detailed guide: {e}"}), 500
681
 
682
  @app.route('/api/projects', methods=['GET'])
683
  def list_projects():