rairo commited on
Commit
4cade00
·
verified ·
1 Parent(s): 6bc6aa5

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +188 -64
main.py CHANGED
@@ -1098,58 +1098,70 @@ def approve_project_plan(project_id):
1098
  return jsonify({'error': 'Project not found or access denied'}), 404
1099
 
1100
  selected_option = request.json.get('selectedOption')
 
1101
 
 
 
1102
  try:
1103
  response = requests.get(project_data['userImageURL'], timeout=30)
1104
  response.raise_for_status()
 
 
 
 
 
 
 
 
 
1105
  pil_image = Image.open(io.BytesIO(response.content)).convert('RGB')
1106
  except Exception as e:
1107
- logger.error(f"[PROJECT APPROVAL] Image download/processing failed: {e}")
1108
  return jsonify({'error': 'Failed to process project image'}), 500
1109
 
 
 
 
 
 
1110
  context = (
1111
  f"The user chose the upcycling project: '{selected_option}'."
1112
  if selected_option
1113
  else f"The user has approved the plan for '{project_data['projectTitle']}'."
1114
  )
1115
 
1116
- # ------------------------------------------------------------------
1117
- # Expanded system prompt for 2026 – covers fashion, electronics, etc.
1118
- # ------------------------------------------------------------------
1119
  detailed_prompt = f"""
1120
- You are Alfred, a world-class DIY and lifestyle expert. The user wants to proceed with the
1121
- project titled "{project_data['projectTitle']}" in the category "{project_data.get('category', 'DIY')}".
1122
  {context}
1123
-
1124
- Provide a detailed, practical guide. For EVERY step you MUST provide a clear illustrative image
1125
- that shows the action being performed.
1126
-
1127
- Category-specific guidance:
1128
- - Fashion & Clothing Repair: specify stitch type, thread colour, needle gauge, and fabric handling tips.
1129
- - Electronics & PCB Repair: include ESD safety warnings, soldering temperatures, and component reference numbers.
1130
- - Beauty & Personal Care DIY: note skin-type suitability and patch-test reminders.
1131
- - Furniture Restoration: mention wood grain direction, sanding grits, and cure times.
1132
- - All categories: prioritise safety, sustainability, and locally available materials.
1133
-
1134
  Format your response EXACTLY like this:
1135
  TOOLS AND MATERIALS:
1136
- - Tool or Material A
1137
- - Tool or Material B
1138
- STEPS (Maximum 5 steps):
1139
  1. First step instructions.
1140
- 2. Second step instructions.
1141
  """
 
 
1142
 
1143
  try:
1144
- chat = client.chats.create(
 
 
 
 
1145
  model=GENERATION_MODEL,
1146
  config=types.GenerateContentConfig(response_modalities=["Text", "Image"])
1147
  )
1148
  full_resp = chat.send_message([detailed_prompt, pil_image])
1149
- ai_time = time.time() - start_time
1150
  logger.info(f"[PROJECT APPROVAL] AI generation completed in {ai_time:.3f}s")
1151
 
1152
- gen_parts = full_resp.candidates[0].content.parts
 
 
 
1153
  combined_text = ""
1154
  inline_images = []
1155
  for part in gen_parts:
@@ -1160,57 +1172,140 @@ def approve_project_plan(project_id):
1160
  inline_images.append(img)
1161
 
1162
  combined_text = combined_text.strip()
 
 
1163
 
1164
- tools_match = re.search(
1165
- r"TOOLS AND MATERIALS:\s*(.*?)\s*(?=STEPS\s*[\(\:]|$)",
1166
- combined_text, re.DOTALL | re.IGNORECASE
1167
- )
1168
- steps_match = re.search(
1169
- r"STEPS\s*\([^)]*\):\s*(.*)|STEPS\s*:\s*(.*)",
1170
- combined_text, re.DOTALL | re.IGNORECASE
1171
- )
 
 
 
 
 
 
 
 
1172
 
1173
- if not tools_match or not steps_match:
1174
- logger.error(f"[PROJECT APPROVAL] Parse error. Response: {combined_text[:500]}")
1175
- return jsonify({'error': 'AI response format error – could not parse sections'}), 500
 
1176
 
1177
  tools_section = tools_match.group(1).strip()
1178
- steps_section = (steps_match.group(1) or steps_match.group(2)).strip()
1179
 
1180
- tools_list = []
1181
- seen = set()
1182
- for line in tools_section.split('\n'):
1183
- item = line.strip("- ").strip()
1184
- if item and item not in seen:
1185
- seen.add(item)
1186
- tools_list.append(item)
 
 
 
 
 
 
 
 
1187
 
1188
  parsed_steps = parse_numbered_steps(steps_section)
1189
 
1190
- if not tools_list or not parsed_steps:
1191
- return jsonify({'error': 'AI response format error – empty tools or steps'}), 500
 
 
 
 
 
 
 
 
 
 
1192
 
1193
  if len(parsed_steps) != len(inline_images):
1194
- min_len = min(len(parsed_steps), len(inline_images))
1195
- parsed_steps = parsed_steps[:min_len]
1196
- inline_images = inline_images[:min_len]
1197
- if min_len == 0:
1198
- return jsonify({'error': 'AI response mismatch – no valid steps found'}), 500
1199
-
1200
- final_steps = []
1201
- for i, step_info in enumerate(parsed_steps):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1202
  try:
 
1203
  img_byte_arr = io.BytesIO()
1204
- inline_images[i].save(img_byte_arr, format='JPEG', optimize=True, quality=70)
1205
- img_path = f"users/{uid}/projects/{project_id}/steps/step_{i+1}_image.jpg"
1206
- img_url = upload_to_storage(img_byte_arr.getvalue(), img_path, 'image/jpeg')
1207
- narration_url = generate_tts_audio_and_upload(step_info['text'], uid, project_id, i + 1)
1208
- step_info.update({"imageUrl": img_url, "narrationUrl": narration_url, "isDone": False, "notes": ""})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1209
  except Exception as e:
1210
- logger.error(f"[PROJECT APPROVAL] Error processing step {i+1}: {e}")
1211
- step_info.update({"imageUrl": "", "narrationUrl": "", "isDone": False, "notes": ""})
1212
- final_steps.append(step_info)
 
 
 
 
 
 
1213
 
 
 
 
 
 
 
 
 
 
 
 
 
1214
  update_data = {
1215
  "status": "ready",
1216
  "toolsList": tools_list,
@@ -1218,17 +1313,46 @@ def approve_project_plan(project_id):
1218
  "selectedOption": selected_option or ""
1219
  }
1220
  project_ref.update(update_data)
1221
- user_ref.update({'credits': user_data.get('credits', 0) - 5})
 
 
1222
 
 
 
1223
  updated_project = project_ref.get()
1224
  updated_project["projectId"] = project_id
 
 
1225
 
 
 
 
 
 
 
 
1226
  total_time = time.time() - start_time
1227
- logger.info(f"[PROJECT APPROVAL] SUCCESS in {total_time:.3f}s")
 
 
 
 
 
 
 
 
 
 
 
 
1228
  return jsonify(updated_project)
1229
 
1230
  except Exception as e:
1231
- logger.error(f"[PROJECT APPROVAL] Exception: {traceback.format_exc()}")
 
 
 
 
1232
  return jsonify({'error': f'Internal server error: {str(e)}'}), 500
1233
 
1234
 
 
1098
  return jsonify({'error': 'Project not found or access denied'}), 404
1099
 
1100
  selected_option = request.json.get('selectedOption')
1101
+ logger.info(f"[PROJECT APPROVAL] Selected option: {selected_option}")
1102
 
1103
+ # Image download and processing timing
1104
+ image_download_start = time.time()
1105
  try:
1106
  response = requests.get(project_data['userImageURL'], timeout=30)
1107
  response.raise_for_status()
1108
+ except requests.RequestException as e:
1109
+ logger.error(f"[PROJECT APPROVAL] ERROR: Image download failed: {e}")
1110
+ return jsonify({'error': 'Failed to download project image'}), 500
1111
+
1112
+ image_download_time = time.time() - image_download_start
1113
+ logger.info(f"[PROJECT APPROVAL] Image download completed in {image_download_time:.3f}s, size: {len(response.content)} bytes")
1114
+
1115
+ image_processing_start = time.time()
1116
+ try:
1117
  pil_image = Image.open(io.BytesIO(response.content)).convert('RGB')
1118
  except Exception as e:
1119
+ logger.error(f"[PROJECT APPROVAL] ERROR: Image processing failed: {e}")
1120
  return jsonify({'error': 'Failed to process project image'}), 500
1121
 
1122
+ image_processing_time = time.time() - image_processing_start
1123
+ logger.info(f"[PROJECT APPROVAL] Image processing completed in {image_processing_time:.3f}s")
1124
+
1125
+ # Context preparation timing
1126
+ context_start = time.time()
1127
  context = (
1128
  f"The user chose the upcycling project: '{selected_option}'."
1129
  if selected_option
1130
  else f"The user has approved the plan for '{project_data['projectTitle']}'."
1131
  )
1132
 
 
 
 
1133
  detailed_prompt = f"""
1134
+ You are a DIY expert. The user wants to proceed with the project titled "{project_data['projectTitle']}".
 
1135
  {context}
1136
+ Provide a detailed guide. For each step, you MUST provide a simple, clear illustrative image.
 
 
 
 
 
 
 
 
 
 
1137
  Format your response EXACTLY like this:
1138
  TOOLS AND MATERIALS:
1139
+ - Tool A
1140
+ - Material B
1141
+ STEPS(Maximum 5 steps):
1142
  1. First step instructions.
1143
+ 2. Second step instructions...
1144
  """
1145
+ context_time = time.time() - context_start
1146
+ logger.info(f"[PROJECT APPROVAL] Context preparation completed in {context_time:.3f}s")
1147
 
1148
  try:
1149
+ # AI generation timing
1150
+ ai_start = time.time()
1151
+ logger.info(f"[PROJECT APPROVAL] Starting AI generation with model: {GENERATION_MODEL}")
1152
+
1153
+ chat = client.chats.create(
1154
  model=GENERATION_MODEL,
1155
  config=types.GenerateContentConfig(response_modalities=["Text", "Image"])
1156
  )
1157
  full_resp = chat.send_message([detailed_prompt, pil_image])
1158
+ ai_time = time.time() - ai_start
1159
  logger.info(f"[PROJECT APPROVAL] AI generation completed in {ai_time:.3f}s")
1160
 
1161
+ # Response parsing timing
1162
+ parsing_start = time.time()
1163
+ gen_parts = full_resp.candidates[0].content.parts
1164
+
1165
  combined_text = ""
1166
  inline_images = []
1167
  for part in gen_parts:
 
1172
  inline_images.append(img)
1173
 
1174
  combined_text = combined_text.strip()
1175
+ parsing_time = time.time() - parsing_start
1176
+ logger.info(f"[PROJECT APPROVAL] Response parsing completed in {parsing_time:.3f}s, found {len(inline_images)} images")
1177
 
1178
+ # Text extraction timing with robust error handling
1179
+ extraction_start = time.time()
1180
+
1181
+ logger.info(f"[PROJECT APPROVAL] AI Response structure check:")
1182
+ logger.info(f"[PROJECT APPROVAL] Full response length: {len(combined_text)}")
1183
+ logger.info(f"[PROJECT APPROVAL] Contains 'TOOLS AND MATERIALS': {'TOOLS AND MATERIALS' in combined_text.upper()}")
1184
+ logger.info(f"[PROJECT APPROVAL] Contains 'STEPS': {'STEPS' in combined_text.upper()}")
1185
+ logger.info(f"[PROJECT APPROVAL] Response preview: {combined_text[:300]}...")
1186
+
1187
+ tools_match = re.search(r"TOOLS AND MATERIALS:\s*(.*?)\s*(?=STEPS\s*\(|STEPS\s*:|$)", combined_text, re.DOTALL | re.IGNORECASE)
1188
+ steps_match = re.search(r"STEPS\s*\([^)]*\):\s*(.*)|STEPS\s*:\s*(.*)", combined_text, re.DOTALL | re.IGNORECASE)
1189
+
1190
+ if not tools_match:
1191
+ logger.error(f"[PROJECT APPROVAL] ERROR: Could not find TOOLS AND MATERIALS section in AI response")
1192
+ logger.error(f"[PROJECT APPROVAL] AI Response full text: {combined_text}")
1193
+ return jsonify({'error': 'AI response format error: Could not parse tools section'}), 500
1194
 
1195
+ if not steps_match:
1196
+ logger.error(f"[PROJECT APPROVAL] ERROR: Could not find STEPS section in AI response")
1197
+ logger.error(f"[PROJECT APPROVAL] AI Response full text: {combined_text}")
1198
+ return jsonify({'error': 'AI response format error: Could not parse steps section'}), 500
1199
 
1200
  tools_section = tools_match.group(1).strip()
1201
+ steps_section = (steps_match.group(1) or steps_match.group(2)).strip() if steps_match else ""
1202
 
1203
+ if not tools_section:
1204
+ logger.error(f"[PROJECT APPROVAL] ERROR: Empty tools section found")
1205
+ return jsonify({'error': 'AI response format error: Empty tools section'}), 500
1206
+
1207
+ if not steps_section:
1208
+ logger.error(f"[PROJECT APPROVAL] ERROR: Empty steps section found")
1209
+ return jsonify({'error': 'AI response format error: Empty steps section'}), 500
1210
+
1211
+ tools_list = [line.strip("- ").strip() for line in tools_section.split('\n') if line.strip() and not line.strip().startswith('-')]
1212
+ dash_tools = [line.strip("- ").strip() for line in tools_section.split('\n') if line.strip().startswith('-')]
1213
+ tools_list.extend(dash_tools)
1214
+
1215
+ # Remove duplicates while preserving order
1216
+ seen = set()
1217
+ tools_list = [x for x in tools_list if not (x in seen or seen.add(x))]
1218
 
1219
  parsed_steps = parse_numbered_steps(steps_section)
1220
 
1221
+ if not tools_list:
1222
+ logger.error(f"[PROJECT APPROVAL] ERROR: No tools parsed from response")
1223
+ logger.error(f"[PROJECT APPROVAL] Tools section was: {tools_section}")
1224
+ return jsonify({'error': 'AI response format error: No tools found'}), 500
1225
+
1226
+ if not parsed_steps:
1227
+ logger.error(f"[PROJECT APPROVAL] ERROR: No steps parsed from response")
1228
+ logger.error(f"[PROJECT APPROVAL] Steps section was: {steps_section}")
1229
+ return jsonify({'error': 'AI response format error: No steps found'}), 500
1230
+
1231
+ extraction_time = time.time() - extraction_start
1232
+ logger.info(f"[PROJECT APPROVAL] Text extraction completed in {extraction_time:.3f}s, tools: {len(tools_list)}, steps: {len(parsed_steps)}")
1233
 
1234
  if len(parsed_steps) != len(inline_images):
1235
+ logger.error(f"[PROJECT APPROVAL] ERROR: AI response mismatch - Steps: {len(parsed_steps)}, Images: {len(inline_images)}")
1236
+ min_length = min(len(parsed_steps), len(inline_images))
1237
+ if min_length > 0:
1238
+ logger.info(f"[PROJECT APPROVAL] Attempting to proceed with {min_length} steps/images")
1239
+ parsed_steps = parsed_steps[:min_length]
1240
+ inline_images = inline_images[:min_length]
1241
+ else:
1242
+ return jsonify({'error': 'AI response mismatch: No valid steps and images found.'}), 500
1243
+
1244
+ # ---------------------------------------------------------------
1245
+ # Step processing – image upload and TTS run IN PARALLEL per step
1246
+ # using ThreadPoolExecutor so Nano Banana 2's longer generation
1247
+ # time doesn't stack on top of Deepgram latency.
1248
+ # ---------------------------------------------------------------
1249
+ import concurrent.futures
1250
+
1251
+ step_processing_start = time.time()
1252
+ final_steps = [None] * len(parsed_steps)
1253
+ total_upload_time = 0
1254
+ total_tts_time = 0
1255
+
1256
+ def process_single_step(args):
1257
+ """Runs image upload and TTS concurrently for one step, returns completed step_info."""
1258
+ i, step_info, pil_img = args
1259
+ result = dict(step_info) # shallow copy so we don't mutate shared state
1260
+ img_url = ""
1261
+ narration_url = ""
1262
+
1263
  try:
1264
+ # Encode JPEG in this thread before spawning sub-futures
1265
  img_byte_arr = io.BytesIO()
1266
+ pil_img.save(img_byte_arr, format='JPEG', optimize=True, quality=70)
1267
+ img_bytes_val = img_byte_arr.getvalue()
1268
+ img_path = f"users/{uid}/projects/{project_id}/steps/step_{i+1}_image.jpg"
1269
+
1270
+ # Fire image upload and TTS simultaneously
1271
+ with concurrent.futures.ThreadPoolExecutor(max_workers=2) as inner_pool:
1272
+ img_upload_start = time.time()
1273
+ tts_start = time.time()
1274
+
1275
+ img_future = inner_pool.submit(upload_to_storage, img_bytes_val, img_path, 'image/jpeg')
1276
+ tts_future = inner_pool.submit(generate_tts_audio_and_upload, result['text'], uid, project_id, i + 1)
1277
+
1278
+ img_url = img_future.result()
1279
+ img_upload_time = time.time() - img_upload_start
1280
+ logger.info(f"[PROJECT APPROVAL] Step {i+1} image upload completed in {img_upload_time:.3f}s")
1281
+
1282
+ narration_url = tts_future.result()
1283
+ tts_time = time.time() - tts_start
1284
+ logger.info(f"[PROJECT APPROVAL] Step {i+1} TTS generation completed in {tts_time:.3f}s")
1285
+
1286
  except Exception as e:
1287
+ logger.error(f"[PROJECT APPROVAL] ERROR processing step {i+1}: {e}")
1288
+
1289
+ result.update({
1290
+ "imageUrl": img_url,
1291
+ "narrationUrl": narration_url,
1292
+ "isDone": False,
1293
+ "notes": ""
1294
+ })
1295
+ return i, result
1296
 
1297
+ # Process all steps in parallel (up to 5 steps × 2 sub-tasks each)
1298
+ step_args = [(i, parsed_steps[i], inline_images[i]) for i in range(len(parsed_steps))]
1299
+
1300
+ with concurrent.futures.ThreadPoolExecutor(max_workers=len(parsed_steps)) as outer_pool:
1301
+ for i, completed_step in outer_pool.map(process_single_step, step_args):
1302
+ final_steps[i] = completed_step
1303
+
1304
+ step_processing_time = time.time() - step_processing_start
1305
+ logger.info(f"[PROJECT APPROVAL] All steps processing completed in {step_processing_time:.3f}s")
1306
+
1307
+ # Database update timing
1308
+ db_update_start = time.time()
1309
  update_data = {
1310
  "status": "ready",
1311
  "toolsList": tools_list,
 
1313
  "selectedOption": selected_option or ""
1314
  }
1315
  project_ref.update(update_data)
1316
+ logger.info(f"[PROJECT APPROVAL] Updating data in db: {len(update_data)} fields")
1317
+ db_update_time = time.time() - db_update_start
1318
+ logger.info(f"[PROJECT APPROVAL] Database update completed in {db_update_time:.3f}s")
1319
 
1320
+ # Final project fetch timing
1321
+ final_fetch_start = time.time()
1322
  updated_project = project_ref.get()
1323
  updated_project["projectId"] = project_id
1324
+ final_fetch_time = time.time() - final_fetch_start
1325
+ logger.info(f"[PROJECT APPROVAL] Final project fetch completed in {final_fetch_time:.3f}s")
1326
 
1327
+ # Credits deduction timing
1328
+ credits_update_start = time.time()
1329
+ user_ref.update({'credits': user_data.get('credits', 0) - 5})
1330
+ credits_update_time = time.time() - credits_update_start
1331
+ logger.info(f"[PROJECT APPROVAL] Credits update completed in {credits_update_time:.3f}s")
1332
+
1333
+ # Total time calculation
1334
  total_time = time.time() - start_time
1335
+ logger.info(f"[PROJECT APPROVAL] SUCCESS: Project approval completed in {total_time:.3f}s")
1336
+ logger.info(f"[PROJECT APPROVAL] TIMING BREAKDOWN:")
1337
+ logger.info(f"[PROJECT APPROVAL] - Image download: {image_download_time:.3f}s")
1338
+ logger.info(f"[PROJECT APPROVAL] - Image processing: {image_processing_time:.3f}s")
1339
+ logger.info(f"[PROJECT APPROVAL] - Context prep: {context_time:.3f}s")
1340
+ logger.info(f"[PROJECT APPROVAL] - AI generation (Nano Banana 2): {ai_time:.3f}s")
1341
+ logger.info(f"[PROJECT APPROVAL] - Response parsing: {parsing_time:.3f}s")
1342
+ logger.info(f"[PROJECT APPROVAL] - Text extraction: {extraction_time:.3f}s")
1343
+ logger.info(f"[PROJECT APPROVAL] - Step processing (threaded): {step_processing_time:.3f}s")
1344
+ logger.info(f"[PROJECT APPROVAL] - DB update: {db_update_time:.3f}s")
1345
+ logger.info(f"[PROJECT APPROVAL] - Final fetch: {final_fetch_time:.3f}s")
1346
+ logger.info(f"[PROJECT APPROVAL] - Credits update: {credits_update_time:.3f}s")
1347
+
1348
  return jsonify(updated_project)
1349
 
1350
  except Exception as e:
1351
+ total_time = time.time() - start_time
1352
+ logger.error(f"[PROJECT APPROVAL] ERROR: Exception occurred after {total_time:.3f}s: {e}")
1353
+ logger.error(f"[PROJECT APPROVAL] Error type: {type(e).__name__}")
1354
+ logger.error(f"[PROJECT APPROVAL] Project ID: {project_id}, User ID: {uid}")
1355
+ logger.error(f"[PROJECT APPROVAL] Full traceback: {traceback.format_exc()}")
1356
  return jsonify({'error': f'Internal server error: {str(e)}'}), 500
1357
 
1358