habulaj commited on
Commit
1403125
·
verified ·
1 Parent(s): 418872d

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +93 -30
main.py CHANGED
@@ -217,29 +217,74 @@ class ProcessUrlRequest(BaseModel):
217
  raw_url: Optional[bool] = True
218
 
219
  @app.post("/process-url")
220
- async def process_url_endpoint(request: ProcessUrlRequest):
221
  """
222
- Processa um post do Instagram a partir de uma URL, sem cadastrar no Supabase.
223
- Chama o agente 'process' da conta especificada em `version`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  """
225
  if not client:
226
- raise HTTPException(status_code=500, detail="Gemini client is not initialized")
 
227
 
228
  temp_file = None
229
  cropped_file_path = None
230
  cropped_video_path = None
231
  screenshot_path = None
 
 
 
 
 
 
 
232
 
233
  try:
234
  from agent_config import AGENTS
235
  account = request.version
236
  if account not in AGENTS:
237
- raise HTTPException(status_code=400, detail=f"Conta '{account}' não configurada.")
238
  agent_conf = AGENTS[account]["process"]
239
  agent_name = agent_conf["name"]
240
 
241
  # 1. Chamar a API externa para obter informações do post
242
- print(f"🔗 Buscando dados do post via API externa: {request.url}")
243
  api_headers = {
244
  "accept": "*/*",
245
  "accept-language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7",
@@ -256,7 +301,7 @@ async def process_url_endpoint(request: ProcessUrlRequest):
256
  timeout=60,
257
  )
258
  if not api_resp.ok:
259
- raise HTTPException(status_code=502, detail=f"Erro na API externa: {api_resp.text}")
260
 
261
  api_data = api_resp.json()
262
  video_url = api_data.get("url")
@@ -264,7 +309,7 @@ async def process_url_endpoint(request: ProcessUrlRequest):
264
  comments = api_data.get("comments", [])
265
 
266
  if not video_url:
267
- raise HTTPException(status_code=502, detail="API externa não retornou URL de mídia.")
268
 
269
  print(f"✅ Mídia obtida: {video_url}")
270
 
@@ -272,7 +317,19 @@ async def process_url_endpoint(request: ProcessUrlRequest):
272
  print(f"📥 Baixando mídia: {video_url}")
273
  response = download_file_with_retry(video_url, timeout=600)
274
  content_type = response.headers.get("content-type", "").lower()
275
- if "image" in content_type:
 
 
 
 
 
 
 
 
 
 
 
 
276
  if "png" in content_type: ext = ".png"
277
  elif "webp" in content_type: ext = ".webp"
278
  else: ext = ".jpg"
@@ -289,7 +346,7 @@ async def process_url_endpoint(request: ProcessUrlRequest):
289
  files_to_send = [video_path_to_analyze]
290
 
291
  # 3. Crop
292
- if "image" in content_type:
293
  print("✂️ Processando imagem: detectando e cortando...")
294
  try:
295
  cropped_file_path = detect_and_crop_image(video_path_to_analyze)
@@ -320,7 +377,7 @@ async def process_url_endpoint(request: ProcessUrlRequest):
320
  if isinstance(c, dict) and (text := c.get("text", "").strip()):
321
  comentarios_add += f"- {text} ({c.get('like_count', 0)} curtidas)\n"
322
 
323
- if "image" in content_type:
324
  tipo_conteudo_add = "\n\nCONTEXTO DO CONTEÚDO: Este post é uma IMAGEM. O título vai aparecer em cima da imagem."
325
  else:
326
  tipo_conteudo_add = ""
@@ -337,26 +394,26 @@ async def process_url_endpoint(request: ProcessUrlRequest):
337
 
338
  # 5. Gerar com Gemini
339
  model_obj = get_gemini_model("flash")
340
- print(f"🧠 Enviando para Gemini (flash) [{agent_name}]...")
341
  response_gemini = await client.generate_content(prompt, files=files_to_send, model=model_obj)
342
 
343
  titles_data = extract_json_from_text(response_gemini.text)
344
  if not titles_data:
345
- return JSONResponse(
346
- content={"raw_content": response_gemini.text, "error": "Failed to parse JSON"},
347
- status_code=200,
348
- )
349
 
350
  result_json = titles_data if isinstance(titles_data, list) else [titles_data]
351
 
352
- # 6. Video export (mesma lógica do /process/{account})
353
  final_content_url = None
354
  srt_for_export = None
 
 
355
  if result_json:
356
  result_data = result_json[0] if isinstance(result_json[0], dict) else {}
357
  title_text = result_data.get("title", "")
358
 
359
- if "image" not in content_type and title_text:
360
  title_text = title_text[0].upper() + title_text[1:] if title_text else title_text
361
  try:
362
  video_for_export = (
@@ -394,7 +451,6 @@ async def process_url_endpoint(request: ProcessUrlRequest):
394
 
395
  if screenshot_url:
396
  # Upload vídeo cortado
397
- cropped_video_url = None
398
  if video_for_export != temp_file.name:
399
  print("☁️ Enviando vídeo cortado para recurve-save...")
400
  with open(video_for_export, "rb") as vf:
@@ -405,9 +461,9 @@ async def process_url_endpoint(request: ProcessUrlRequest):
405
  timeout=120,
406
  )
407
  vid_upload_resp.raise_for_status()
408
- cropped_video_url = vid_upload_resp.json().get("url", "")
409
 
410
- export_video_url = cropped_video_url if cropped_video_url else video_url
411
 
412
  import cv2
413
  cap = cv2.VideoCapture(video_for_export)
@@ -566,7 +622,7 @@ LEGENDA ORIGINAL:
566
  print(f"⚠️ Erro no video export: {ve}")
567
  raise Exception(f"Falha no video export: {ve}")
568
 
569
- elif "image" in content_type and title_text and result_data.get("result_type") == "meme":
570
  try:
571
  img_for_meme = (
572
  cropped_file_path
@@ -586,13 +642,20 @@ LEGENDA ORIGINAL:
586
  if final_content_url and isinstance(result_json[0], dict):
587
  result_json[0]["final_content_url"] = final_content_url
588
 
589
- return result_json
 
 
 
 
 
 
 
 
 
 
590
 
591
- except HTTPException:
592
- raise
593
  except Exception as e:
594
- print(f"⚠️ Erro em /process-url: {e}")
595
- raise HTTPException(status_code=500, detail=str(e))
596
  finally:
597
  if temp_file and os.path.exists(temp_file.name): os.unlink(temp_file.name)
598
  if cropped_file_path and os.path.exists(cropped_file_path): os.unlink(cropped_file_path)
@@ -630,7 +693,7 @@ async def process_account_endpoint(account: str):
630
  system_discord_id = AGENTS[account].get("system_discord_id", 0)
631
 
632
  # Buscar 1 post aprovado pelo filtro mas ainda não processado
633
- select_url = f"{supabase_url}/rest/v1/posts?select=*&account_target=eq.{account}&approved_filter=eq.true&result=is.null&limit=1"
634
  res_get = requests.get(select_url, headers=headers, timeout=10)
635
  if not res_get.ok:
636
  raise HTTPException(status_code=500, detail=f"Erro ao ler posts: {res_get.text}")
@@ -1223,7 +1286,7 @@ async def run_filter_account(account: str):
1223
  system_discord_id = AGENTS[account].get("system_discord_id", 0)
1224
 
1225
  # Buscar 1 post pendente para filtro para essa conta
1226
- select_url = f"{supabase_url}/rest/v1/posts?select=*&account_target=eq.{account}&filter_message=is.null&limit=1"
1227
  res_get = requests.get(select_url, headers=headers, timeout=10)
1228
  if not res_get.ok:
1229
  raise HTTPException(status_code=500, detail=f"Erro ao ler posts: {res_get.text}")
@@ -1429,7 +1492,7 @@ async def publish_account_endpoint(account: str):
1429
  system_discord_id = AGENTS[account].get("system_discord_id", 0)
1430
 
1431
  # Buscar 1 post pronto para publicação
1432
- select_url = f"{supabase_url}/rest/v1/posts?select=*&account_target=eq.{account}&result=not.is.null&final_content_url=not.is.null&published=eq.false&or=(superior_needs_verification.is.null,superior_needs_verification.eq.false)&limit=1"
1433
  res_get = requests.get(select_url, headers=headers, timeout=10)
1434
  if not res_get.ok:
1435
  raise HTTPException(status_code=500, detail=f"Erro ao ler posts: {res_get.text}")
 
217
  raw_url: Optional[bool] = True
218
 
219
  @app.post("/process-url")
220
+ async def process_url_endpoint(request: ProcessUrlRequest, background_tasks: BackgroundTasks):
221
  """
222
+ Cria um registro inicial no Supabase e envia para processamento em background.
223
+ """
224
+ supabase_url = os.getenv("SUPABASE_URL", "").rstrip("/")
225
+ supabase_key = os.getenv("SUPABASE_KEY", "")
226
+ if not supabase_url or not supabase_key:
227
+ raise HTTPException(status_code=500, detail="Credenciais do Supabase não configuradas no ambiente.")
228
+
229
+ headers = {
230
+ "apikey": supabase_key,
231
+ "Authorization": f"Bearer {supabase_key}",
232
+ "Content-Type": "application/json",
233
+ "Prefer": "return=representation"
234
+ }
235
+
236
+ # Inserir no Supabase com user_created=True
237
+ post_payload = {
238
+ "ig_post_url": request.url,
239
+ "ig_caption": request.context,
240
+ "account_target": request.version,
241
+ "user_created": True,
242
+ "published": False
243
+ }
244
+
245
+ res_post = requests.post(f"{supabase_url}/rest/v1/posts", headers=headers, json=post_payload, timeout=10)
246
+ if not res_post.ok:
247
+ raise HTTPException(status_code=500, detail=f"Erro ao criar registro no Supabase: {res_post.text}")
248
+
249
+ records = res_post.json()
250
+ record_id = records[0].get("id") if records else None
251
+
252
+ if not record_id:
253
+ raise HTTPException(status_code=500, detail="Registro criado, mas ID não retornado pelo Supabase.")
254
+
255
+ background_tasks.add_task(run_process_url, request, record_id)
256
+ return {"status": "ok", "message": "Solicitação em processamento no background.", "record_id": record_id}
257
+
258
+ async def run_process_url(request: ProcessUrlRequest, record_id: int):
259
+ """
260
+ Processa um post do Instagram a partir de uma URL em background, salvando o resultado.
261
  """
262
  if not client:
263
+ print("Gemini client is not initialized")
264
+ return
265
 
266
  temp_file = None
267
  cropped_file_path = None
268
  cropped_video_path = None
269
  screenshot_path = None
270
+ supabase_url = os.getenv("SUPABASE_URL", "").rstrip("/")
271
+ supabase_key = os.getenv("SUPABASE_KEY", "")
272
+ headers = {
273
+ "apikey": supabase_key,
274
+ "Authorization": f"Bearer {supabase_key}",
275
+ "Content-Type": "application/json"
276
+ }
277
 
278
  try:
279
  from agent_config import AGENTS
280
  account = request.version
281
  if account not in AGENTS:
282
+ raise Exception(f"Conta '{account}' não configurada.")
283
  agent_conf = AGENTS[account]["process"]
284
  agent_name = agent_conf["name"]
285
 
286
  # 1. Chamar a API externa para obter informações do post
287
+ print(f"🔗 Buscando dados do post via API externa (process-url): {request.url}")
288
  api_headers = {
289
  "accept": "*/*",
290
  "accept-language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7",
 
301
  timeout=60,
302
  )
303
  if not api_resp.ok:
304
+ raise Exception(f"Erro na API externa: {api_resp.text}")
305
 
306
  api_data = api_resp.json()
307
  video_url = api_data.get("url")
 
309
  comments = api_data.get("comments", [])
310
 
311
  if not video_url:
312
+ raise Exception("API externa não retornou URL de mídia.")
313
 
314
  print(f"✅ Mídia obtida: {video_url}")
315
 
 
317
  print(f"📥 Baixando mídia: {video_url}")
318
  response = download_file_with_retry(video_url, timeout=600)
319
  content_type = response.headers.get("content-type", "").lower()
320
+ is_image = "image" in content_type
321
+
322
+ post_type = "image" if is_image else "video"
323
+
324
+ # Atualizar type e ig_post_url com o que baixamos
325
+ patch_initial = {
326
+ "type": post_type,
327
+ "ig_post_url": video_url,
328
+ "ig_caption": context
329
+ }
330
+ requests.patch(f"{supabase_url}/rest/v1/posts?id=eq.{record_id}", headers=headers, json=patch_initial)
331
+
332
+ if is_image:
333
  if "png" in content_type: ext = ".png"
334
  elif "webp" in content_type: ext = ".webp"
335
  else: ext = ".jpg"
 
346
  files_to_send = [video_path_to_analyze]
347
 
348
  # 3. Crop
349
+ if is_image:
350
  print("✂️ Processando imagem: detectando e cortando...")
351
  try:
352
  cropped_file_path = detect_and_crop_image(video_path_to_analyze)
 
377
  if isinstance(c, dict) and (text := c.get("text", "").strip()):
378
  comentarios_add += f"- {text} ({c.get('like_count', 0)} curtidas)\n"
379
 
380
+ if is_image:
381
  tipo_conteudo_add = "\n\nCONTEXTO DO CONTEÚDO: Este post é uma IMAGEM. O título vai aparecer em cima da imagem."
382
  else:
383
  tipo_conteudo_add = ""
 
394
 
395
  # 5. Gerar com Gemini
396
  model_obj = get_gemini_model("flash")
397
+ print(f"🧠 Enviando para Gemini (flash) [{agent_name}] process-url...")
398
  response_gemini = await client.generate_content(prompt, files=files_to_send, model=model_obj)
399
 
400
  titles_data = extract_json_from_text(response_gemini.text)
401
  if not titles_data:
402
+ print("Failed to parse JSON")
403
+ return
 
 
404
 
405
  result_json = titles_data if isinstance(titles_data, list) else [titles_data]
406
 
407
+ # 6. Video export
408
  final_content_url = None
409
  srt_for_export = None
410
+ exported_cropped_video_url = None
411
+
412
  if result_json:
413
  result_data = result_json[0] if isinstance(result_json[0], dict) else {}
414
  title_text = result_data.get("title", "")
415
 
416
+ if not is_image and title_text:
417
  title_text = title_text[0].upper() + title_text[1:] if title_text else title_text
418
  try:
419
  video_for_export = (
 
451
 
452
  if screenshot_url:
453
  # Upload vídeo cortado
 
454
  if video_for_export != temp_file.name:
455
  print("☁️ Enviando vídeo cortado para recurve-save...")
456
  with open(video_for_export, "rb") as vf:
 
461
  timeout=120,
462
  )
463
  vid_upload_resp.raise_for_status()
464
+ exported_cropped_video_url = vid_upload_resp.json().get("url", "")
465
 
466
+ export_video_url = exported_cropped_video_url if exported_cropped_video_url else video_url
467
 
468
  import cv2
469
  cap = cv2.VideoCapture(video_for_export)
 
622
  print(f"⚠️ Erro no video export: {ve}")
623
  raise Exception(f"Falha no video export: {ve}")
624
 
625
+ elif is_image and title_text and result_data.get("result_type") == "meme":
626
  try:
627
  img_for_meme = (
628
  cropped_file_path
 
642
  if final_content_url and isinstance(result_json[0], dict):
643
  result_json[0]["final_content_url"] = final_content_url
644
 
645
+ # Salvar o resultado no banco
646
+ patch_final = {
647
+ "result": result_json
648
+ }
649
+ if final_content_url:
650
+ patch_final["final_content_url"] = final_content_url
651
+ if exported_cropped_video_url:
652
+ patch_final["crop_content_url"] = exported_cropped_video_url
653
+
654
+ requests.patch(f"{supabase_url}/rest/v1/posts?id=eq.{record_id}", headers=headers, json=patch_final)
655
+ print(f"✅ /process-url via Background task concluído! Record: {record_id}")
656
 
 
 
657
  except Exception as e:
658
+ print(f"⚠️ Erro em run_process_url background: {e}")
 
659
  finally:
660
  if temp_file and os.path.exists(temp_file.name): os.unlink(temp_file.name)
661
  if cropped_file_path and os.path.exists(cropped_file_path): os.unlink(cropped_file_path)
 
693
  system_discord_id = AGENTS[account].get("system_discord_id", 0)
694
 
695
  # Buscar 1 post aprovado pelo filtro mas ainda não processado
696
+ select_url = f"{supabase_url}/rest/v1/posts?select=*&account_target=eq.{account}&approved_filter=eq.true&result=is.null&user_created=eq.false&limit=1"
697
  res_get = requests.get(select_url, headers=headers, timeout=10)
698
  if not res_get.ok:
699
  raise HTTPException(status_code=500, detail=f"Erro ao ler posts: {res_get.text}")
 
1286
  system_discord_id = AGENTS[account].get("system_discord_id", 0)
1287
 
1288
  # Buscar 1 post pendente para filtro para essa conta
1289
+ select_url = f"{supabase_url}/rest/v1/posts?select=*&account_target=eq.{account}&filter_message=is.null&user_created=eq.false&limit=1"
1290
  res_get = requests.get(select_url, headers=headers, timeout=10)
1291
  if not res_get.ok:
1292
  raise HTTPException(status_code=500, detail=f"Erro ao ler posts: {res_get.text}")
 
1492
  system_discord_id = AGENTS[account].get("system_discord_id", 0)
1493
 
1494
  # Buscar 1 post pronto para publicação
1495
+ select_url = f"{supabase_url}/rest/v1/posts?select=*&account_target=eq.{account}&result=not.is.null&final_content_url=not.is.null&published=eq.false&user_created=eq.false&or=(superior_needs_verification.is.null,superior_needs_verification.eq.false)&limit=1"
1496
  res_get = requests.get(select_url, headers=headers, timeout=10)
1497
  if not res_get.ok:
1498
  raise HTTPException(status_code=500, detail=f"Erro ao ler posts: {res_get.text}")