Update main.py
Browse files
main.py
CHANGED
|
@@ -264,6 +264,9 @@ async def run_process_url(request: ProcessUrlRequest, record_id: int):
|
|
| 264 |
print("Gemini client is not initialized")
|
| 265 |
return
|
| 266 |
|
|
|
|
|
|
|
|
|
|
| 267 |
temp_file = None
|
| 268 |
cropped_file_path = None
|
| 269 |
cropped_video_path = None
|
|
@@ -342,13 +345,14 @@ async def run_process_url(request: ProcessUrlRequest, record_id: int):
|
|
| 342 |
if chunk:
|
| 343 |
temp_file.write(chunk)
|
| 344 |
temp_file.close()
|
|
|
|
| 345 |
|
| 346 |
video_path_to_analyze = temp_file.name
|
| 347 |
-
files_to_send = [
|
| 348 |
|
| 349 |
# 3. Crop
|
| 350 |
if is_image:
|
| 351 |
-
print("✂️ Processando imagem: detectando e cortando...")
|
| 352 |
try:
|
| 353 |
cropped_file_path = detect_and_crop_image(video_path_to_analyze)
|
| 354 |
if cropped_file_path and os.path.exists(cropped_file_path):
|
|
@@ -356,18 +360,43 @@ async def run_process_url(request: ProcessUrlRequest, record_id: int):
|
|
| 356 |
except Exception as e:
|
| 357 |
print(f"⚠️ Erro ao cortar imagem: {e}")
|
| 358 |
else:
|
| 359 |
-
print("✂️ Processando vídeo: detectando e cortando bordas...")
|
| 360 |
try:
|
| 361 |
cropped_video_path = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4").name
|
| 362 |
-
|
| 363 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
files_to_send.append(cropped_video_path)
|
| 365 |
-
print("✅ Vídeo cortado
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
else:
|
| 367 |
cropped_video_path = None
|
| 368 |
except Exception as e:
|
| 369 |
cropped_video_path = None
|
| 370 |
-
print(f"⚠️ Erro
|
|
|
|
|
|
|
|
|
|
| 371 |
|
| 372 |
# 4. Montar contextos
|
| 373 |
contexto_add = f"\n{context}" if context else ""
|
|
@@ -395,8 +424,9 @@ async def run_process_url(request: ProcessUrlRequest, record_id: int):
|
|
| 395 |
|
| 396 |
# 5. Gerar com Gemini
|
| 397 |
model_obj = get_gemini_model("flash")
|
| 398 |
-
print(f"🧠 Enviando para Gemini (flash) [{agent_name}] process-url...")
|
| 399 |
response_gemini = await client.generate_content(prompt, files=files_to_send, model=model_obj)
|
|
|
|
| 400 |
|
| 401 |
titles_data = extract_json_from_text(response_gemini.text)
|
| 402 |
if not titles_data:
|
|
@@ -451,9 +481,9 @@ async def run_process_url(request: ProcessUrlRequest, record_id: int):
|
|
| 451 |
screenshot_url = upload_resp.json().get("url", "")
|
| 452 |
|
| 453 |
if screenshot_url:
|
| 454 |
-
# Upload vídeo cortado
|
| 455 |
-
if video_for_export != temp_file.name:
|
| 456 |
-
print("☁️ Enviando vídeo cortado para recurve-save...")
|
| 457 |
with open(video_for_export, "rb") as vf:
|
| 458 |
vid_upload_resp = requests.post(
|
| 459 |
"https://habulaj-recurve-save.hf.space/upload",
|
|
@@ -463,9 +493,10 @@ async def run_process_url(request: ProcessUrlRequest, record_id: int):
|
|
| 463 |
)
|
| 464 |
vid_upload_resp.raise_for_status()
|
| 465 |
exported_cropped_video_url = vid_upload_resp.json().get("url", "")
|
| 466 |
-
|
| 467 |
export_video_url = exported_cropped_video_url if exported_cropped_video_url else video_url
|
| 468 |
-
|
|
|
|
| 469 |
import cv2
|
| 470 |
cap = cv2.VideoCapture(video_for_export)
|
| 471 |
crop_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) if cap.isOpened() else 1080
|
|
@@ -486,7 +517,7 @@ async def run_process_url(request: ProcessUrlRequest, record_id: int):
|
|
| 486 |
# Legendas
|
| 487 |
needs_legenda = result_data.get("legenda", False)
|
| 488 |
if needs_legenda:
|
| 489 |
-
print("🎙️ Gerando legendas (Groq + Gemini)...")
|
| 490 |
try:
|
| 491 |
srt_raw, _, _, _ = await get_groq_srt_base(
|
| 492 |
export_video_url, language="en", temperature=0.4, has_bg_music=False
|
|
@@ -709,8 +740,10 @@ async def process_account_endpoint(account: str):
|
|
| 709 |
publish_res = await safe_call_publish(account)
|
| 710 |
return {"status": "ok", "message": "Nenhuma postagem pendente para ser processada.", "publish_result": publish_res}
|
| 711 |
|
|
|
|
| 712 |
record = records[0]
|
| 713 |
record_id = record.get("id")
|
|
|
|
| 714 |
video_url = record.get("ig_post_url")
|
| 715 |
context = record.get("ig_caption", "")
|
| 716 |
comments = record.get("comments") # Se existir no banco, pode ser uma lista
|
|
@@ -758,12 +791,13 @@ async def process_account_endpoint(account: str):
|
|
| 758 |
for chunk in response.iter_content(chunk_size=1024*1024):
|
| 759 |
if chunk: temp_file.write(chunk)
|
| 760 |
temp_file.close()
|
|
|
|
| 761 |
|
| 762 |
video_path_to_analyze = temp_file.name
|
| 763 |
-
files_to_send = [
|
| 764 |
|
| 765 |
if 'image' in content_type:
|
| 766 |
-
print(f"✂️ Processando imagem: detectando e cortando...")
|
| 767 |
try:
|
| 768 |
cropped_file_path = detect_and_crop_image(video_path_to_analyze)
|
| 769 |
if cropped_file_path and os.path.exists(cropped_file_path):
|
|
@@ -772,19 +806,44 @@ async def process_account_endpoint(account: str):
|
|
| 772 |
print(f"⚠️ Erro ao cortar imagem: {e}")
|
| 773 |
else:
|
| 774 |
# Vídeo: detectar e cortar bordas estáticas
|
| 775 |
-
print(f"✂️ Processando vídeo: detectando e cortando bordas...")
|
| 776 |
try:
|
| 777 |
cropped_video_path = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4').name
|
| 778 |
-
|
| 779 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 780 |
files_to_send.append(cropped_video_path)
|
| 781 |
-
print(f"✅ Vídeo cortado
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 782 |
else:
|
| 783 |
cropped_video_path = None
|
| 784 |
-
print(f"⚠️ Crop de vídeo não gerou resultado, seguindo com original apenas")
|
| 785 |
except Exception as e:
|
| 786 |
cropped_video_path = None
|
| 787 |
print(f"⚠️ Erro ao cortar vídeo: {e}")
|
|
|
|
|
|
|
|
|
|
| 788 |
contexto_add = f"\n{context}" if context else ""
|
| 789 |
comentarios_add = ""
|
| 790 |
if comments:
|
|
@@ -813,10 +872,11 @@ async def process_account_endpoint(account: str):
|
|
| 813 |
)
|
| 814 |
model_name = record.get("model", "flash") # Tenta pegar do banco, senão flash
|
| 815 |
model_obj = get_gemini_model(model_name)
|
| 816 |
-
print(f"🧠 Enviando para Gemini ({model_name}) para processamento...")
|
| 817 |
|
| 818 |
-
# Envio do prompt + arquivos (
|
| 819 |
response_gemini = await client.generate_content(prompt, files=files_to_send, model=model_obj)
|
|
|
|
| 820 |
|
| 821 |
titles_data = extract_json_from_text(response_gemini.text)
|
| 822 |
if not titles_data:
|
|
@@ -868,10 +928,9 @@ async def process_account_endpoint(account: str):
|
|
| 868 |
screenshot_url = upload_resp.json().get("url", "")
|
| 869 |
|
| 870 |
if screenshot_url:
|
| 871 |
-
# 3. Upload do vídeo cortado para recurve-save
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
print("☁️ Enviando vídeo cortado para recurve-save...")
|
| 875 |
with open(video_for_export, 'rb') as vf:
|
| 876 |
vid_upload_resp = requests.post(
|
| 877 |
"https://habulaj-recurve-save.hf.space/upload",
|
|
@@ -881,12 +940,13 @@ async def process_account_endpoint(account: str):
|
|
| 881 |
)
|
| 882 |
vid_upload_resp.raise_for_status()
|
| 883 |
cropped_video_url = vid_upload_resp.json().get("url", "")
|
| 884 |
-
print(f"✅ Vídeo cortado enviado: {cropped_video_url}")
|
| 885 |
|
| 886 |
# URL do vídeo para o export: cortado se disponível, senão original
|
| 887 |
export_video_url = cropped_video_url if cropped_video_url else video_url
|
|
|
|
| 888 |
|
| 889 |
# 4. Obter dimensões do vídeo para cálculo horizontal/vertical
|
|
|
|
| 890 |
import cv2
|
| 891 |
cap = cv2.VideoCapture(video_for_export)
|
| 892 |
crop_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) if cap.isOpened() else 1080
|
|
@@ -920,7 +980,7 @@ async def process_account_endpoint(account: str):
|
|
| 920 |
needs_legenda = result_data.get("legenda", False)
|
| 921 |
srt_for_export = None
|
| 922 |
if needs_legenda:
|
| 923 |
-
print("🎙️ Gerando legendas (Groq + Gemini)...")
|
| 924 |
try:
|
| 925 |
# Transcricao via Groq usando o arquivo local
|
| 926 |
import urllib.request as _urlreq
|
|
|
|
| 264 |
print("Gemini client is not initialized")
|
| 265 |
return
|
| 266 |
|
| 267 |
+
t_start = time.time()
|
| 268 |
+
print(f"🚀 [0.0s] Inciando process-url em background para Record: {record_id}")
|
| 269 |
+
|
| 270 |
temp_file = None
|
| 271 |
cropped_file_path = None
|
| 272 |
cropped_video_path = None
|
|
|
|
| 345 |
if chunk:
|
| 346 |
temp_file.write(chunk)
|
| 347 |
temp_file.close()
|
| 348 |
+
print(f"📥 [{time.time()-t_start:.1f}s] Download concluído.")
|
| 349 |
|
| 350 |
video_path_to_analyze = temp_file.name
|
| 351 |
+
files_to_send = [] # Ordem: [cropped, original]
|
| 352 |
|
| 353 |
# 3. Crop
|
| 354 |
if is_image:
|
| 355 |
+
print(f"✂️ [{time.time()-t_start:.1f}s] Processando imagem: detectando e cortando...")
|
| 356 |
try:
|
| 357 |
cropped_file_path = detect_and_crop_image(video_path_to_analyze)
|
| 358 |
if cropped_file_path and os.path.exists(cropped_file_path):
|
|
|
|
| 360 |
except Exception as e:
|
| 361 |
print(f"⚠️ Erro ao cortar imagem: {e}")
|
| 362 |
else:
|
| 363 |
+
print(f"✂️ [{time.time()-t_start:.1f}s] Processando vídeo: detectando e cortando bordas...")
|
| 364 |
try:
|
| 365 |
cropped_video_path = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4").name
|
| 366 |
+
crop_status = detect_and_crop_video(video_path_to_analyze, cropped_video_path, text_cut=request.text_cut)
|
| 367 |
+
|
| 368 |
+
if crop_status == "aborted_area_too_small":
|
| 369 |
+
print(f"🚫 [{time.time()-t_start:.1f}s] Crop abortado: área de texto no centro. Negando filtro.")
|
| 370 |
+
requests.patch(f"{supabase_url}/rest/v1/posts?id=eq.{record_id}", headers=headers, json={
|
| 371 |
+
"approved_filter": False,
|
| 372 |
+
"result": {"error": "Rejeitado: Região útil de texto insuficiente para crop seguro (texto centralizado)."}
|
| 373 |
+
})
|
| 374 |
+
return # Para o processamento aqui
|
| 375 |
+
|
| 376 |
+
if crop_status == "success" and os.path.exists(cropped_video_path):
|
| 377 |
files_to_send.append(cropped_video_path)
|
| 378 |
+
print(f"✅ [{time.time()-t_start:.1f}s] Vídeo cortado com sucesso.")
|
| 379 |
+
|
| 380 |
+
# Upload antecipado para recurve-save (Performance)
|
| 381 |
+
print(f"☁️ [{time.time()-t_start:.1f}s] Enviando vídeo cortado antecipadamente para recurve-save...")
|
| 382 |
+
with open(cropped_video_path, "rb") as vf:
|
| 383 |
+
vid_upload_resp = requests.post(
|
| 384 |
+
"https://habulaj-recurve-save.hf.space/upload",
|
| 385 |
+
files={"files": ("cropped_video.mp4", vf, "video/mp4")},
|
| 386 |
+
data={"long_duration": "yes"},
|
| 387 |
+
timeout=120,
|
| 388 |
+
)
|
| 389 |
+
vid_upload_resp.raise_for_status()
|
| 390 |
+
exported_cropped_video_url = vid_upload_resp.json().get("url", "")
|
| 391 |
+
print(f"✅ [{time.time()-t_start:.1f}s] URL do vídeo cortado: {exported_cropped_video_url}")
|
| 392 |
else:
|
| 393 |
cropped_video_path = None
|
| 394 |
except Exception as e:
|
| 395 |
cropped_video_path = None
|
| 396 |
+
print(f"⚠️ Erro no processo de crop: {e}")
|
| 397 |
+
|
| 398 |
+
# Adiciona o vídeo original (sempre em segundo se houver crop)
|
| 399 |
+
files_to_send.append(video_path_to_analyze)
|
| 400 |
|
| 401 |
# 4. Montar contextos
|
| 402 |
contexto_add = f"\n{context}" if context else ""
|
|
|
|
| 424 |
|
| 425 |
# 5. Gerar com Gemini
|
| 426 |
model_obj = get_gemini_model("flash")
|
| 427 |
+
print(f"🧠 [{time.time()-t_start:.1f}s] Enviando para Gemini (flash) [{agent_name}] process-url...")
|
| 428 |
response_gemini = await client.generate_content(prompt, files=files_to_send, model=model_obj)
|
| 429 |
+
print(f"✨ [{time.time()-t_start:.1f}s] Resposta do Gemini recebida.")
|
| 430 |
|
| 431 |
titles_data = extract_json_from_text(response_gemini.text)
|
| 432 |
if not titles_data:
|
|
|
|
| 481 |
screenshot_url = upload_resp.json().get("url", "")
|
| 482 |
|
| 483 |
if screenshot_url:
|
| 484 |
+
# 3. Upload do vídeo cortado para recurve-save (Se ainda não foi feito)
|
| 485 |
+
if (video_for_export != temp_file.name) and not exported_cropped_video_url:
|
| 486 |
+
print(f"☁️ [{time.time()-t_start:.1f}s] Enviando vídeo cortado para recurve-save...")
|
| 487 |
with open(video_for_export, "rb") as vf:
|
| 488 |
vid_upload_resp = requests.post(
|
| 489 |
"https://habulaj-recurve-save.hf.space/upload",
|
|
|
|
| 493 |
)
|
| 494 |
vid_upload_resp.raise_for_status()
|
| 495 |
exported_cropped_video_url = vid_upload_resp.json().get("url", "")
|
| 496 |
+
|
| 497 |
export_video_url = exported_cropped_video_url if exported_cropped_video_url else video_url
|
| 498 |
+
|
| 499 |
+
print(f"📏 [{time.time()-t_start:.1f}s] Preparando export...")
|
| 500 |
import cv2
|
| 501 |
cap = cv2.VideoCapture(video_for_export)
|
| 502 |
crop_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) if cap.isOpened() else 1080
|
|
|
|
| 517 |
# Legendas
|
| 518 |
needs_legenda = result_data.get("legenda", False)
|
| 519 |
if needs_legenda:
|
| 520 |
+
print(f"🎙️ [{time.time()-t_start:.1f}s] Gerando legendas (Groq + Gemini)...")
|
| 521 |
try:
|
| 522 |
srt_raw, _, _, _ = await get_groq_srt_base(
|
| 523 |
export_video_url, language="en", temperature=0.4, has_bg_music=False
|
|
|
|
| 740 |
publish_res = await safe_call_publish(account)
|
| 741 |
return {"status": "ok", "message": "Nenhuma postagem pendente para ser processada.", "publish_result": publish_res}
|
| 742 |
|
| 743 |
+
t_start = time.time()
|
| 744 |
record = records[0]
|
| 745 |
record_id = record.get("id")
|
| 746 |
+
print(f"🚀 [0.0s] Inciando processamento da conta '{account}' para Record: {record_id}")
|
| 747 |
video_url = record.get("ig_post_url")
|
| 748 |
context = record.get("ig_caption", "")
|
| 749 |
comments = record.get("comments") # Se existir no banco, pode ser uma lista
|
|
|
|
| 791 |
for chunk in response.iter_content(chunk_size=1024*1024):
|
| 792 |
if chunk: temp_file.write(chunk)
|
| 793 |
temp_file.close()
|
| 794 |
+
print(f"📥 [{time.time()-t_start:.1f}s] Download concluído.")
|
| 795 |
|
| 796 |
video_path_to_analyze = temp_file.name
|
| 797 |
+
files_to_send = [] # Ordem: [cropped, original]
|
| 798 |
|
| 799 |
if 'image' in content_type:
|
| 800 |
+
print(f"✂️ [{time.time()-t_start:.1f}s] Processando imagem: detectando e cortando...")
|
| 801 |
try:
|
| 802 |
cropped_file_path = detect_and_crop_image(video_path_to_analyze)
|
| 803 |
if cropped_file_path and os.path.exists(cropped_file_path):
|
|
|
|
| 806 |
print(f"⚠️ Erro ao cortar imagem: {e}")
|
| 807 |
else:
|
| 808 |
# Vídeo: detectar e cortar bordas estáticas
|
| 809 |
+
print(f"✂️ [{time.time()-t_start:.1f}s] Processando vídeo: detectando e cortando bordas...")
|
| 810 |
try:
|
| 811 |
cropped_video_path = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4').name
|
| 812 |
+
crop_status = detect_and_crop_video(video_path_to_analyze, cropped_video_path)
|
| 813 |
+
|
| 814 |
+
if crop_status == "aborted_area_too_small":
|
| 815 |
+
print(f"🚫 [{time.time()-t_start:.1f}s] Crop abortado: área de texto no centro. Negando filtro.")
|
| 816 |
+
requests.patch(f"{supabase_url}/rest/v1/posts?id=eq.{record_id}", headers=headers, json={
|
| 817 |
+
"approved_filter": False,
|
| 818 |
+
"result": {"error": "Rejeitado: Região útil de texto insuficiente para crop seguro (texto centralizado)."}
|
| 819 |
+
})
|
| 820 |
+
return {"status": "rejected", "message": "Postagem rejeitada por crop insuficiente."}
|
| 821 |
+
|
| 822 |
+
if crop_status == "success" and os.path.exists(cropped_video_path):
|
| 823 |
files_to_send.append(cropped_video_path)
|
| 824 |
+
print(f"✅ [{time.perf_counter()-t_start:.1f}s] Vídeo cortado com sucesso.")
|
| 825 |
+
|
| 826 |
+
# Upload antecipado para recurve-save (Performance)
|
| 827 |
+
print(f"☁️ [{time.time()-t_start:.1f}s] Enviando vídeo cortado antecipadamente para recurve-save...")
|
| 828 |
+
with open(cropped_video_path, 'rb') as vf:
|
| 829 |
+
vid_upload_resp = requests.post(
|
| 830 |
+
"https://habulaj-recurve-save.hf.space/upload",
|
| 831 |
+
files={'files': ('cropped_video.mp4', vf, 'video/mp4')},
|
| 832 |
+
data={'long_duration': 'yes'},
|
| 833 |
+
timeout=120
|
| 834 |
+
)
|
| 835 |
+
vid_upload_resp.raise_for_status()
|
| 836 |
+
cropped_video_url = vid_upload_resp.json().get("url", "")
|
| 837 |
+
print(f"✅ [{time.time()-t_start:.1f}s] URL do vídeo cortado: {cropped_video_url}")
|
| 838 |
else:
|
| 839 |
cropped_video_path = None
|
| 840 |
+
print(f"⚠️ [{time.time()-t_start:.1f}s] Crop de vídeo não gerou resultado, seguindo com original apenas")
|
| 841 |
except Exception as e:
|
| 842 |
cropped_video_path = None
|
| 843 |
print(f"⚠️ Erro ao cortar vídeo: {e}")
|
| 844 |
+
|
| 845 |
+
# Adiciona o vídeo original (sempre em segundo se houver crop)
|
| 846 |
+
files_to_send.append(video_path_to_analyze)
|
| 847 |
contexto_add = f"\n{context}" if context else ""
|
| 848 |
comentarios_add = ""
|
| 849 |
if comments:
|
|
|
|
| 872 |
)
|
| 873 |
model_name = record.get("model", "flash") # Tenta pegar do banco, senão flash
|
| 874 |
model_obj = get_gemini_model(model_name)
|
| 875 |
+
print(f"🧠 [{time.time()-t_start:.1f}s] Enviando para Gemini ({model_name}) para processamento...")
|
| 876 |
|
| 877 |
+
# Envio do prompt + arquivos (cortado e original) pro Gemini
|
| 878 |
response_gemini = await client.generate_content(prompt, files=files_to_send, model=model_obj)
|
| 879 |
+
print(f"✨ [{time.time()-t_start:.1f}s] Resposta do Gemini recebida.")
|
| 880 |
|
| 881 |
titles_data = extract_json_from_text(response_gemini.text)
|
| 882 |
if not titles_data:
|
|
|
|
| 928 |
screenshot_url = upload_resp.json().get("url", "")
|
| 929 |
|
| 930 |
if screenshot_url:
|
| 931 |
+
# 3. Upload do vídeo cortado para recurve-save (Se ainda não foi feito)
|
| 932 |
+
if (video_for_export != temp_file.name) and not cropped_video_url:
|
| 933 |
+
print(f"☁️ [{time.time()-t_start:.1f}s] Enviando vídeo cortado para recurve-save...")
|
|
|
|
| 934 |
with open(video_for_export, 'rb') as vf:
|
| 935 |
vid_upload_resp = requests.post(
|
| 936 |
"https://habulaj-recurve-save.hf.space/upload",
|
|
|
|
| 940 |
)
|
| 941 |
vid_upload_resp.raise_for_status()
|
| 942 |
cropped_video_url = vid_upload_resp.json().get("url", "")
|
|
|
|
| 943 |
|
| 944 |
# URL do vídeo para o export: cortado se disponível, senão original
|
| 945 |
export_video_url = cropped_video_url if cropped_video_url else video_url
|
| 946 |
+
print(f"✅ [{time.time()-t_start:.1f}s] Vídeo para export: {export_video_url}")
|
| 947 |
|
| 948 |
# 4. Obter dimensões do vídeo para cálculo horizontal/vertical
|
| 949 |
+
print(f"📏 [{time.time()-t_start:.1f}s] Preparando export...")
|
| 950 |
import cv2
|
| 951 |
cap = cv2.VideoCapture(video_for_export)
|
| 952 |
crop_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) if cap.isOpened() else 1080
|
|
|
|
| 980 |
needs_legenda = result_data.get("legenda", False)
|
| 981 |
srt_for_export = None
|
| 982 |
if needs_legenda:
|
| 983 |
+
print(f"🎙️ [{time.time()-t_start:.1f}s] Gerando legendas (Groq + Gemini)...")
|
| 984 |
try:
|
| 985 |
# Transcricao via Groq usando o arquivo local
|
| 986 |
import urllib.request as _urlreq
|