Spaces:
Running
Running
Update main.py
Browse files
main.py
CHANGED
|
@@ -758,197 +758,216 @@ def create_project():
|
|
| 758 |
return jsonify({'error': f"An error occurred: {e}"}), 500
|
| 759 |
|
| 760 |
|
| 761 |
-
# --------------------------------------------------------
|
| 762 |
@app.route('/api/projects/<string:project_id>/approve', methods=['PUT'])
|
| 763 |
def approve_project_plan(project_id):
|
| 764 |
start_time = time.time()
|
| 765 |
-
logger.info(f"[PROJECT APPROVAL]
|
| 766 |
-
|
| 767 |
-
#
|
| 768 |
-
|
| 769 |
-
# ───────────────────────────────────────────────────────────
|
| 770 |
-
auth_t0 = time.time()
|
| 771 |
uid = verify_token(request.headers.get('Authorization'))
|
| 772 |
if not uid:
|
| 773 |
-
logger.error(f"[PROJECT APPROVAL]
|
| 774 |
return jsonify({'error': 'Unauthorized'}), 401
|
| 775 |
-
auth_time = time.time() -
|
| 776 |
-
logger.info(f"[
|
| 777 |
|
| 778 |
-
#
|
| 779 |
-
|
| 780 |
-
# ───────────────────────────────────────────────────────────
|
| 781 |
-
uc_t0 = time.time()
|
| 782 |
user_ref = db_ref.child(f'users/{uid}')
|
| 783 |
user_data = user_ref.get()
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
logger.error(f"[PROJECT APPROVAL] ❌ Insufficient credits • uid={uid} credits={credits}")
|
| 787 |
return jsonify({'error': 'Insufficient credits'}), 402
|
| 788 |
-
|
| 789 |
-
logger.info(f"[
|
| 790 |
|
| 791 |
-
#
|
| 792 |
-
|
| 793 |
-
# ───────────────────────────────────────────────────────────
|
| 794 |
-
pf_t0 = time.time()
|
| 795 |
project_ref = db_ref.child(f'projects/{project_id}')
|
| 796 |
project_data = project_ref.get()
|
| 797 |
if not project_data or project_data.get('uid') != uid:
|
| 798 |
-
logger.error(f"[PROJECT APPROVAL]
|
| 799 |
return jsonify({'error': 'Project not found or access denied'}), 404
|
| 800 |
-
|
| 801 |
-
logger.info(f"[
|
| 802 |
-
|
| 803 |
-
#
|
| 804 |
-
# 4. DOUBLE-APPROVAL GUARD
|
| 805 |
-
# ───────────────────────────────────────────────────────────
|
| 806 |
-
if project_data.get('status') == 'ready':
|
| 807 |
-
logger.warning(f"[PROJECT APPROVAL] ⏹ Already approved • project={project_id}")
|
| 808 |
-
return jsonify({'error': 'Project is already approved'}), 409
|
| 809 |
-
|
| 810 |
-
# ───────────────────────────────────────────────────────────
|
| 811 |
-
# 5. VALIDATE selectedOption
|
| 812 |
-
# ───────────────────────────────────────────────────────────
|
| 813 |
-
val_t0 = time.time()
|
| 814 |
selected_option = request.json.get('selectedOption')
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
# 6. IMAGE DOWNLOAD / PROCESSING
|
| 831 |
-
# ───────────────────────────────────────────────────────────
|
| 832 |
-
img_dl_t0 = time.time()
|
| 833 |
-
resp = requests.get(project_data['userImageURL'])
|
| 834 |
-
pil_image = Image.open(io.BytesIO(resp.content)).convert('RGB')
|
| 835 |
-
img_dl_time = time.time() - img_dl_t0
|
| 836 |
-
logger.info(f"[TIMING] image_download+proc={img_dl_time:.3f}s • bytes={len(resp.content)}")
|
| 837 |
-
|
| 838 |
-
# ───────────────────────────────────────────────────────────
|
| 839 |
-
# 7. PROMPT + AI GENERATION
|
| 840 |
-
# ───────────────────────────────────────────────────────────
|
| 841 |
-
prompt_t0 = time.time()
|
| 842 |
-
context_line = (
|
| 843 |
f"The user chose the upcycling project: '{selected_option}'."
|
| 844 |
if selected_option
|
| 845 |
else f"The user has approved the plan for '{project_data['projectTitle']}'."
|
| 846 |
)
|
|
|
|
| 847 |
detailed_prompt = f"""
|
| 848 |
You are a DIY expert. The user wants to proceed with the project titled "{project_data['projectTitle']}".
|
| 849 |
-
{
|
| 850 |
Provide a detailed guide. For each step, you MUST provide a simple, clear illustrative image.
|
| 851 |
-
|
| 852 |
TOOLS AND MATERIALS:
|
| 853 |
- Tool A
|
| 854 |
- Material B
|
| 855 |
-
|
| 856 |
STEPS(Maximum 5 steps):
|
| 857 |
1. First step instructions.
|
| 858 |
2. Second step instructions...
|
| 859 |
"""
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
|
| 937 |
-
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 952 |
|
| 953 |
@app.route('/api/projects', methods=['GET'])
|
| 954 |
def list_projects():
|
|
|
|
| 758 |
return jsonify({'error': f"An error occurred: {e}"}), 500
|
| 759 |
|
| 760 |
|
|
|
|
| 761 |
@app.route('/api/projects/<string:project_id>/approve', methods=['PUT'])
|
| 762 |
def approve_project_plan(project_id):
|
| 763 |
start_time = time.time()
|
| 764 |
+
logger.info(f"[PROJECT APPROVAL] Starting approval process for project: {project_id}")
|
| 765 |
+
|
| 766 |
+
# Authorization timing
|
| 767 |
+
auth_start = time.time()
|
|
|
|
|
|
|
| 768 |
uid = verify_token(request.headers.get('Authorization'))
|
| 769 |
if not uid:
|
| 770 |
+
logger.error(f"[PROJECT APPROVAL] ERROR: Unauthorized access attempt for project: {project_id}")
|
| 771 |
return jsonify({'error': 'Unauthorized'}), 401
|
| 772 |
+
auth_time = time.time() - auth_start
|
| 773 |
+
logger.info(f"[PROJECT APPROVAL] Authorization completed in {auth_time:.3f}s for user: {uid}")
|
| 774 |
|
| 775 |
+
# User data fetch timing
|
| 776 |
+
user_fetch_start = time.time()
|
|
|
|
|
|
|
| 777 |
user_ref = db_ref.child(f'users/{uid}')
|
| 778 |
user_data = user_ref.get()
|
| 779 |
+
if not user_data or user_data.get('credits', 0) < 5:
|
| 780 |
+
logger.error(f"[PROJECT APPROVAL] ERROR: Insufficient credits for user: {uid}, credits: {user_data.get('credits', 0) if user_data else 0}")
|
|
|
|
| 781 |
return jsonify({'error': 'Insufficient credits'}), 402
|
| 782 |
+
user_fetch_time = time.time() - user_fetch_start
|
| 783 |
+
logger.info(f"[PROJECT APPROVAL] User data fetch completed in {user_fetch_time:.3f}s, credits: {user_data.get('credits', 0)}")
|
| 784 |
|
| 785 |
+
# Project data fetch timing
|
| 786 |
+
project_fetch_start = time.time()
|
|
|
|
|
|
|
| 787 |
project_ref = db_ref.child(f'projects/{project_id}')
|
| 788 |
project_data = project_ref.get()
|
| 789 |
if not project_data or project_data.get('uid') != uid:
|
| 790 |
+
logger.error(f"[PROJECT APPROVAL] ERROR: Project not found or access denied - project_id: {project_id}, uid: {uid}")
|
| 791 |
return jsonify({'error': 'Project not found or access denied'}), 404
|
| 792 |
+
project_fetch_time = time.time() - project_fetch_start
|
| 793 |
+
logger.info(f"[PROJECT APPROVAL] Project data fetch completed in {project_fetch_time:.3f}s for project: {project_data.get('projectTitle', 'Unknown')}")
|
| 794 |
+
|
| 795 |
+
# Image download and processing timing
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 796 |
selected_option = request.json.get('selectedOption')
|
| 797 |
+
logger.info(f"[PROJECT APPROVAL] Selected option: {selected_option}")
|
| 798 |
+
|
| 799 |
+
image_download_start = time.time()
|
| 800 |
+
response = requests.get(project_data['userImageURL'])
|
| 801 |
+
image_download_time = time.time() - image_download_start
|
| 802 |
+
logger.info(f"[PROJECT APPROVAL] Image download completed in {image_download_time:.3f}s, size: {len(response.content)} bytes")
|
| 803 |
+
|
| 804 |
+
image_processing_start = time.time()
|
| 805 |
+
pil_image = Image.open(io.BytesIO(response.content)).convert('RGB')
|
| 806 |
+
image_processing_time = time.time() - image_processing_start
|
| 807 |
+
logger.info(f"[PROJECT APPROVAL] Image processing completed in {image_processing_time:.3f}s")
|
| 808 |
+
|
| 809 |
+
# Context preparation timing
|
| 810 |
+
context_start = time.time()
|
| 811 |
+
context = (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 812 |
f"The user chose the upcycling project: '{selected_option}'."
|
| 813 |
if selected_option
|
| 814 |
else f"The user has approved the plan for '{project_data['projectTitle']}'."
|
| 815 |
)
|
| 816 |
+
|
| 817 |
detailed_prompt = f"""
|
| 818 |
You are a DIY expert. The user wants to proceed with the project titled "{project_data['projectTitle']}".
|
| 819 |
+
{context}
|
| 820 |
Provide a detailed guide. For each step, you MUST provide a simple, clear illustrative image.
|
| 821 |
+
Format your response EXACTLY like this:
|
| 822 |
TOOLS AND MATERIALS:
|
| 823 |
- Tool A
|
| 824 |
- Material B
|
|
|
|
| 825 |
STEPS(Maximum 5 steps):
|
| 826 |
1. First step instructions.
|
| 827 |
2. Second step instructions...
|
| 828 |
"""
|
| 829 |
+
context_time = time.time() - context_start
|
| 830 |
+
logger.info(f"[PROJECT APPROVAL] Context preparation completed in {context_time:.3f}s")
|
| 831 |
+
|
| 832 |
+
try:
|
| 833 |
+
# AI generation timing
|
| 834 |
+
ai_start = time.time()
|
| 835 |
+
logger.info(f"[PROJECT APPROVAL] Starting AI generation with model: {GENERATION_MODEL}")
|
| 836 |
+
|
| 837 |
+
chat = client.chats.create(
|
| 838 |
+
model=GENERATION_MODEL,
|
| 839 |
+
config=types.GenerateContentConfig(response_modalities=["Text", "Image"])
|
| 840 |
+
)
|
| 841 |
+
full_resp = chat.send_message([detailed_prompt, pil_image])
|
| 842 |
+
ai_time = time.time() - ai_start
|
| 843 |
+
logger.info(f"[PROJECT APPROVAL] AI generation completed in {ai_time:.3f}s")
|
| 844 |
+
|
| 845 |
+
# Response parsing timing
|
| 846 |
+
parsing_start = time.time()
|
| 847 |
+
gen_parts = full_resp.candidates[0].content.parts
|
| 848 |
+
|
| 849 |
+
combined_text = ""
|
| 850 |
+
inline_images = []
|
| 851 |
+
for part in gen_parts:
|
| 852 |
+
if part.text is not None:
|
| 853 |
+
combined_text += part.text + "\n"
|
| 854 |
+
if part.inline_data is not None:
|
| 855 |
+
img = Image.open(io.BytesIO(part.inline_data.data)).convert('RGB')
|
| 856 |
+
inline_images.append(img)
|
| 857 |
+
|
| 858 |
+
combined_text = combined_text.strip()
|
| 859 |
+
parsing_time = time.time() - parsing_start
|
| 860 |
+
logger.info(f"[PROJECT APPROVAL] Response parsing completed in {parsing_time:.3f}s, found {len(inline_images)} images")
|
| 861 |
+
|
| 862 |
+
# Text extraction timing
|
| 863 |
+
extraction_start = time.time()
|
| 864 |
+
tools_section = re.search(r"TOOLS AND MATERIALS:\s*(.*?)\s*STEPS:", combined_text, re.DOTALL).group(1).strip()
|
| 865 |
+
steps_section = re.search(r"STEPS:\s*(.*)", combined_text, re.DOTALL).group(1).strip()
|
| 866 |
+
|
| 867 |
+
tools_list = [line.strip("- ").strip() for line in tools_section.split('\n') if line.strip()]
|
| 868 |
+
parsed_steps = parse_numbered_steps(steps_section)
|
| 869 |
+
extraction_time = time.time() - extraction_start
|
| 870 |
+
logger.info(f"[PROJECT APPROVAL] Text extraction completed in {extraction_time:.3f}s, tools: {len(tools_list)}, steps: {len(parsed_steps)}")
|
| 871 |
+
|
| 872 |
+
if len(parsed_steps) != len(inline_images):
|
| 873 |
+
logger.error(f"[PROJECT APPROVAL] ERROR: AI response mismatch - Steps: {len(parsed_steps)}, Images: {len(inline_images)}")
|
| 874 |
+
return jsonify({'error': 'AI response mismatch: Steps and images do not match.'}), 500
|
| 875 |
+
|
| 876 |
+
# Step processing timing
|
| 877 |
+
step_processing_start = time.time()
|
| 878 |
+
final_steps = []
|
| 879 |
+
total_upload_time = 0
|
| 880 |
+
total_tts_time = 0
|
| 881 |
+
|
| 882 |
+
for i, step_info in enumerate(parsed_steps):
|
| 883 |
+
logger.info(f"[PROJECT APPROVAL] Processing step {i+1}/{len(parsed_steps)}")
|
| 884 |
+
|
| 885 |
+
# Image upload timing
|
| 886 |
+
image_upload_start = time.time()
|
| 887 |
+
img_byte_arr = io.BytesIO()
|
| 888 |
+
inline_images[i].save(img_byte_arr, format='JPEG', optimize=True, quality=70)
|
| 889 |
+
img_path = f"users/{uid}/projects/{project_id}/steps/step_{i+1}_image.jpg"
|
| 890 |
+
img_url = upload_to_storage(img_byte_arr.getvalue(), img_path, 'image/jpeg')
|
| 891 |
+
image_upload_time = time.time() - image_upload_start
|
| 892 |
+
total_upload_time += image_upload_time
|
| 893 |
+
logger.info(f"[PROJECT APPROVAL] Step {i+1} image upload completed in {image_upload_time:.3f}s")
|
| 894 |
+
|
| 895 |
+
# TTS generation timing
|
| 896 |
+
tts_start = time.time()
|
| 897 |
+
narration_url = generate_tts_audio_and_upload(step_info['text'], uid, project_id, i + 1)
|
| 898 |
+
tts_time = time.time() - tts_start
|
| 899 |
+
total_tts_time += tts_time
|
| 900 |
+
logger.info(f"[PROJECT APPROVAL] Step {i+1} TTS generation completed in {tts_time:.3f}s")
|
| 901 |
+
|
| 902 |
+
step_info.update({
|
| 903 |
+
"imageUrl": img_url,
|
| 904 |
+
"narrationUrl": narration_url,
|
| 905 |
+
"isDone": False,
|
| 906 |
+
"notes": ""
|
| 907 |
+
})
|
| 908 |
+
final_steps.append(step_info)
|
| 909 |
+
|
| 910 |
+
step_processing_time = time.time() - step_processing_start
|
| 911 |
+
logger.info(f"[PROJECT APPROVAL] All steps processing completed in {step_processing_time:.3f}s")
|
| 912 |
+
logger.info(f"[PROJECT APPROVAL] Total upload time: {total_upload_time:.3f}s, Total TTS time: {total_tts_time:.3f}s")
|
| 913 |
+
|
| 914 |
+
# Database update timing
|
| 915 |
+
db_update_start = time.time()
|
| 916 |
+
update_data = {
|
| 917 |
+
"status": "ready",
|
| 918 |
+
"toolsList": tools_list,
|
| 919 |
+
"steps": final_steps,
|
| 920 |
+
"selectedOption": selected_option or "",
|
| 921 |
+
#update upcycling options to ensure only selected option persists
|
| 922 |
+
"upcyclingOptions": [selected_option] if selected_option else []
|
| 923 |
+
}
|
| 924 |
+
project_ref.update(update_data)
|
| 925 |
+
db_update_time = time.time() - db_update_start
|
| 926 |
+
logger.info(f"[PROJECT APPROVAL] Database update completed in {db_update_time:.3f}s")
|
| 927 |
+
|
| 928 |
+
# Final project fetch timing
|
| 929 |
+
final_fetch_start = time.time()
|
| 930 |
+
updated_project = project_ref.get()
|
| 931 |
+
updated_project["projectId"] = project_id
|
| 932 |
+
final_fetch_time = time.time() - final_fetch_start
|
| 933 |
+
logger.info(f"[PROJECT APPROVAL] Final project fetch completed in {final_fetch_time:.3f}s")
|
| 934 |
+
|
| 935 |
+
# Credits deduction timing
|
| 936 |
+
credits_update_start = time.time()
|
| 937 |
+
user_ref.update({'credits': user_data.get('credits', 0) - 5})
|
| 938 |
+
credits_update_time = time.time() - credits_update_start
|
| 939 |
+
logger.info(f"[PROJECT APPROVAL] Credits update completed in {credits_update_time:.3f}s")
|
| 940 |
+
|
| 941 |
+
# Total time calculation
|
| 942 |
+
total_time = time.time() - start_time
|
| 943 |
+
logger.info(f"[PROJECT APPROVAL] SUCCESS: Project approval completed in {total_time:.3f}s")
|
| 944 |
+
logger.info(f"[PROJECT APPROVAL] TIMING BREAKDOWN:")
|
| 945 |
+
logger.info(f"[PROJECT APPROVAL] - Authorization: {auth_time:.3f}s")
|
| 946 |
+
logger.info(f"[PROJECT APPROVAL] - User fetch: {user_fetch_time:.3f}s")
|
| 947 |
+
logger.info(f"[PROJECT APPROVAL] - Project fetch: {project_fetch_time:.3f}s")
|
| 948 |
+
logger.info(f"[PROJECT APPROVAL] - Image download: {image_download_time:.3f}s")
|
| 949 |
+
logger.info(f"[PROJECT APPROVAL] - Image processing: {image_processing_time:.3f}s")
|
| 950 |
+
logger.info(f"[PROJECT APPROVAL] - Context prep: {context_time:.3f}s")
|
| 951 |
+
logger.info(f"[PROJECT APPROVAL] - AI generation: {ai_time:.3f}s")
|
| 952 |
+
logger.info(f"[PROJECT APPROVAL] - Response parsing: {parsing_time:.3f}s")
|
| 953 |
+
logger.info(f"[PROJECT APPROVAL] - Text extraction: {extraction_time:.3f}s")
|
| 954 |
+
logger.info(f"[PROJECT APPROVAL] - Step processing: {step_processing_time:.3f}s")
|
| 955 |
+
logger.info(f"[PROJECT APPROVAL] - Total uploads: {total_upload_time:.3f}s")
|
| 956 |
+
logger.info(f"[PROJECT APPROVAL] - Total TTS: {total_tts_time:.3f}s")
|
| 957 |
+
logger.info(f"[PROJECT APPROVAL] - DB update: {db_update_time:.3f}s")
|
| 958 |
+
logger.info(f"[PROJECT APPROVAL] - Final fetch: {final_fetch_time:.3f}s")
|
| 959 |
+
logger.info(f"[PROJECT APPROVAL] - Credits update: {credits_update_time:.3f}s")
|
| 960 |
+
|
| 961 |
+
return jsonify(updated_project)
|
| 962 |
+
|
| 963 |
+
except Exception as e:
|
| 964 |
+
total_time = time.time() - start_time
|
| 965 |
+
logger.error(f"[PROJECT APPROVAL] ERROR: Exception occurred after {total_time:.3f}s: {e}")
|
| 966 |
+
logger.error(f"[PROJECT APPROVAL] Error type: {type(e).__name__}")
|
| 967 |
+
logger.error(f"[PROJECT APPROVAL] Project ID: {project_id}, User ID: {uid}")
|
| 968 |
+
import traceback
|
| 969 |
+
logger.error(f"[PROJECT APPROVAL] Full traceback: {traceback.format_exc()}")
|
| 970 |
+
return jsonify({'error': str(e)}), 500
|
| 971 |
|
| 972 |
@app.route('/api/projects', methods=['GET'])
|
| 973 |
def list_projects():
|