Update routers/editor.py
Browse files- routers/editor.py +35 -4
routers/editor.py
CHANGED
|
@@ -231,18 +231,23 @@ async def editor_chat(request: EditorChatRequest):
|
|
| 231 |
with open(context_path, "w", encoding="utf-8") as f:
|
| 232 |
json.dump(request.project, f, ensure_ascii=False)
|
| 233 |
|
| 234 |
-
print(f"🤖 [EDITOR]
|
| 235 |
model_obj = get_gemini_model("flash")
|
| 236 |
response = await g.client.generate_content(prompt, files=[context_path], model=model_obj)
|
|
|
|
| 237 |
|
| 238 |
data = extract_json_from_text(response.text)
|
| 239 |
if not data or not isinstance(data, dict) or "reply" not in data:
|
|
|
|
| 240 |
return {"reply": response.text, "actions": []}
|
| 241 |
|
| 242 |
actions = data.get("actions")
|
| 243 |
if not isinstance(actions, list):
|
| 244 |
actions = []
|
| 245 |
|
|
|
|
|
|
|
|
|
|
| 246 |
return {"reply": data.get("reply", ""), "actions": actions}
|
| 247 |
|
| 248 |
except Exception as e:
|
|
@@ -366,11 +371,22 @@ async def editor_smart_cut(request: SmartCutRequest):
|
|
| 366 |
original_media_to_delete = None
|
| 367 |
context_path = None
|
| 368 |
try:
|
| 369 |
-
|
|
|
|
|
|
|
| 370 |
request.video_url, time_start=request.window_start, time_end=request.window_end,
|
| 371 |
)
|
| 372 |
original_media_to_delete = original_media_path
|
| 373 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
# get_groq_srt_base's timestamps are relative to the TRIMMED window
|
| 375 |
# (0-based) — shift back to the source file's own absolute time, the
|
| 376 |
# same space Element.videoStartOffset already uses, so the frontend
|
|
@@ -378,27 +394,42 @@ async def editor_smart_cut(request: SmartCutRequest):
|
|
| 378 |
srt_absolute = shift_srt_timestamps(srt_base, request.window_start)
|
| 379 |
transcript_blocks = parse_srt(srt_absolute)
|
| 380 |
if not transcript_blocks:
|
|
|
|
| 381 |
return {"segments": [], "summary": "I couldn't find any speech in this clip to analyze."}
|
| 382 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
temp_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static", "temp")
|
| 384 |
os.makedirs(temp_dir, exist_ok=True)
|
| 385 |
context_path = os.path.join(temp_dir, f"smart_cut_transcript_{uuid.uuid4().hex[:8]}.json")
|
| 386 |
with open(context_path, "w", encoding="utf-8") as f:
|
| 387 |
json.dump(transcript_blocks, f, ensure_ascii=False)
|
| 388 |
|
| 389 |
-
print(f"
|
| 390 |
prompt = build_smart_cut_prompt(request.instruction)
|
| 391 |
model_obj = get_gemini_model("flash")
|
| 392 |
response = await g.client.generate_content(prompt, files=[context_path], model=model_obj)
|
|
|
|
| 393 |
|
| 394 |
data = extract_json_from_text(response.text)
|
| 395 |
if not data or not isinstance(data, dict):
|
|
|
|
| 396 |
return {"segments": [], "summary": "Couldn't make sense of the speech analysis — try again."}
|
| 397 |
|
| 398 |
raw_segments = data.get("segments")
|
| 399 |
-
|
|
|
|
| 400 |
summary = data.get("summary") or ""
|
| 401 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
return {"segments": segments, "summary": summary}
|
| 403 |
|
| 404 |
except Exception as e:
|
|
|
|
| 231 |
with open(context_path, "w", encoding="utf-8") as f:
|
| 232 |
json.dump(request.project, f, ensure_ascii=False)
|
| 233 |
|
| 234 |
+
print(f"🤖 [EDITOR/CHAT] Mensagem: {request.message!r} | histórico: {len(request.history or [])} turno(s) | elementos no snapshot: {len(request.project.get('elements', []))}")
|
| 235 |
model_obj = get_gemini_model("flash")
|
| 236 |
response = await g.client.generate_content(prompt, files=[context_path], model=model_obj)
|
| 237 |
+
print(f"🤖 [EDITOR/CHAT] Resposta bruta do Gemini:\n{response.text}")
|
| 238 |
|
| 239 |
data = extract_json_from_text(response.text)
|
| 240 |
if not data or not isinstance(data, dict) or "reply" not in data:
|
| 241 |
+
print(f"⚠️ [EDITOR/CHAT] Resposta não veio em JSON válido com 'reply' — devolvendo o texto bruto sem ações.")
|
| 242 |
return {"reply": response.text, "actions": []}
|
| 243 |
|
| 244 |
actions = data.get("actions")
|
| 245 |
if not isinstance(actions, list):
|
| 246 |
actions = []
|
| 247 |
|
| 248 |
+
action_types = [a.get("type") for a in actions if isinstance(a, dict)]
|
| 249 |
+
print(f"🤖 [EDITOR/CHAT] reply={data.get('reply', '')!r} | {len(actions)} ação(ões): {action_types}")
|
| 250 |
+
|
| 251 |
return {"reply": data.get("reply", ""), "actions": actions}
|
| 252 |
|
| 253 |
except Exception as e:
|
|
|
|
| 371 |
original_media_to_delete = None
|
| 372 |
context_path = None
|
| 373 |
try:
|
| 374 |
+
print(f"🎬 [SMART-CUT] video_url={request.video_url} | janela=[{request.window_start:.2f}, {request.window_end:.2f}] | instrução={request.instruction!r}")
|
| 375 |
+
|
| 376 |
+
srt_base, _, processed_audio_url, _word_level, original_media_path = await get_groq_srt_base(
|
| 377 |
request.video_url, time_start=request.window_start, time_end=request.window_end,
|
| 378 |
)
|
| 379 |
original_media_to_delete = original_media_path
|
| 380 |
|
| 381 |
+
# get_groq_srt_base's own `processed_audio_url` drops the "processed/"
|
| 382 |
+
# path segment (a pre-existing bug in media.py — unused by every
|
| 383 |
+
# other caller today, so never noticed: neither /subtitle nor
|
| 384 |
+
# /subtitle/groq put it in their response). Corrected just for this
|
| 385 |
+
# log line rather than touching shared code for an unrelated fix.
|
| 386 |
+
audio_log_url = processed_audio_url.replace("/static/", "/static/processed/", 1) if processed_audio_url else None
|
| 387 |
+
print(f"🔊 [SMART-CUT] Áudio processado (o que o Whisper de fato ouviu): {audio_log_url}")
|
| 388 |
+
print(f"🎙️ [SMART-CUT] Transcrição bruta (relativa à janela recortada):\n{srt_base}")
|
| 389 |
+
|
| 390 |
# get_groq_srt_base's timestamps are relative to the TRIMMED window
|
| 391 |
# (0-based) — shift back to the source file's own absolute time, the
|
| 392 |
# same space Element.videoStartOffset already uses, so the frontend
|
|
|
|
| 394 |
srt_absolute = shift_srt_timestamps(srt_base, request.window_start)
|
| 395 |
transcript_blocks = parse_srt(srt_absolute)
|
| 396 |
if not transcript_blocks:
|
| 397 |
+
print("⚠️ [SMART-CUT] Nenhum bloco de fala reconhecido nessa janela — abortando sem chamar o Gemini.")
|
| 398 |
return {"segments": [], "summary": "I couldn't find any speech in this clip to analyze."}
|
| 399 |
|
| 400 |
+
print(f"🕒 [SMART-CUT] Transcrição em tempo absoluto ({len(transcript_blocks)} blocos):")
|
| 401 |
+
for b in transcript_blocks:
|
| 402 |
+
print(f" [{b['start']:.2f} - {b['end']:.2f}] {b['text']}")
|
| 403 |
+
|
| 404 |
temp_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static", "temp")
|
| 405 |
os.makedirs(temp_dir, exist_ok=True)
|
| 406 |
context_path = os.path.join(temp_dir, f"smart_cut_transcript_{uuid.uuid4().hex[:8]}.json")
|
| 407 |
with open(context_path, "w", encoding="utf-8") as f:
|
| 408 |
json.dump(transcript_blocks, f, ensure_ascii=False)
|
| 409 |
|
| 410 |
+
print(f"🤖 [SMART-CUT] Pedindo pro Gemini escolher os melhores trechos...")
|
| 411 |
prompt = build_smart_cut_prompt(request.instruction)
|
| 412 |
model_obj = get_gemini_model("flash")
|
| 413 |
response = await g.client.generate_content(prompt, files=[context_path], model=model_obj)
|
| 414 |
+
print(f"🤖 [SMART-CUT] Resposta bruta do Gemini:\n{response.text}")
|
| 415 |
|
| 416 |
data = extract_json_from_text(response.text)
|
| 417 |
if not data or not isinstance(data, dict):
|
| 418 |
+
print("⚠️ [SMART-CUT] Resposta do Gemini não veio em JSON válido.")
|
| 419 |
return {"segments": [], "summary": "Couldn't make sense of the speech analysis — try again."}
|
| 420 |
|
| 421 |
raw_segments = data.get("segments")
|
| 422 |
+
raw_segments = raw_segments if isinstance(raw_segments, list) else []
|
| 423 |
+
segments = snap_segments_to_transcript(raw_segments, transcript_blocks)
|
| 424 |
summary = data.get("summary") or ""
|
| 425 |
|
| 426 |
+
print(f"✂️ [SMART-CUT] Gemini propôs {len(raw_segments)} segmento(s); {len(segments)} passaram na validação de timestamp.")
|
| 427 |
+
if len(segments) < len(raw_segments):
|
| 428 |
+
print(f"⚠️ [SMART-CUT] {len(raw_segments) - len(segments)} segmento(s) descartado(s) — start/end não bateu com nenhum bloco real da transcrição (tolerância 0.35s). Possível timestamp alucinado. Bruto: {raw_segments}")
|
| 429 |
+
for s in segments:
|
| 430 |
+
print(f" MANTIDO [{s['start']:.2f} - {s['end']:.2f}] \"{s['text']}\" — motivo: {s['reason']}")
|
| 431 |
+
print(f"📦 [SMART-CUT] Resumo devolvido: {summary!r}")
|
| 432 |
+
|
| 433 |
return {"segments": segments, "summary": summary}
|
| 434 |
|
| 435 |
except Exception as e:
|