habulaj commited on
Commit
3f0a293
·
verified ·
1 Parent(s): 5695cc6

Update routers/editor.py

Browse files
Files changed (1) hide show
  1. routers/editor.py +155 -2
routers/editor.py CHANGED
@@ -36,6 +36,14 @@ class SmartCutRequest(BaseModel):
36
  window_end: float
37
  instruction: str
38
 
 
 
 
 
 
 
 
 
39
 
40
  # ── Template catalog (hand-mirrored from src/components/common/templates.ts)
41
  # ─────────────────────────────────────────────────────────────────────────
@@ -142,12 +150,35 @@ CATÁLOGO DE AÇÕES DISPONÍVEIS (campos entre colchetes são opcionais):
142
  "elementId" tem que ser um elemento de vídeo real do arquivo anexado
143
  (nunca um `refId` de um elemento que você acabou de criar). "instruction"
144
  é o pedido do usuário sobre COMO cortar, repassado da forma mais fiel
145
- possível. Essa ação é DIFERENTE de todas as outras: não é aplicada na
146
  hora — o editor ainda vai transcrever a fala e analisar o que manter,
147
  o que pode levar cerca de um minuto. Por isso, quando usar essa ação,
148
  seu "reply" deve deixar isso claro (ex: "Vou ouvir o vídeo e separar as
149
  melhores falas — isso pode levar um minuto."), nunca afirmar que o corte
150
- já foi feito. No máximo UMA ação smart_cut_video por resposta.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
  TEMPLATES DISPONÍVEIS (use o "id" exato em insert_template.templateId; os ids
153
  dentro de "editableFields" podem ser usados como chave em "overrides"):
@@ -188,6 +219,9 @@ REGRAS IMPORTANTES:
188
  de elemento, alterar o código/comportamento do próprio editor, acessar
189
  informação de fora do que foi anexado). Cortar vídeo com base na fala JÁ é
190
  possível — ver ação 10 (smart_cut_video) — não trate isso como limitação.
 
 
 
191
  Se o usuário pedir algo fora do catálogo, ou qualquer coisa
192
  que você não tenha certeza de conseguir fazer com o catálogo, diga isso de
193
  forma CLARA, HONESTA e EDUCADA no "reply" — algo como "ainda não fui
@@ -435,6 +469,125 @@ async def editor_smart_cut(request: SmartCutRequest):
435
  except Exception as e:
436
  print(f"❌ Erro no /editor/smart-cut: {e}")
437
  raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
  finally:
439
  if original_media_to_delete and os.path.exists(original_media_to_delete) and "static" in original_media_to_delete:
440
  try:
 
36
  window_end: float
37
  instruction: str
38
 
39
+ class AnalyzeMediaRequest(BaseModel):
40
+ media_url: str
41
+ # Same coordinate space as SmartCutRequest above — the clip's current
42
+ # trim window in the source file's timeline, not the whole source file.
43
+ window_start: float
44
+ window_end: float
45
+ instruction: str
46
+
47
 
48
  # ── Template catalog (hand-mirrored from src/components/common/templates.ts)
49
  # ─────────────────────────────────────────────────────────────────────────
 
150
  "elementId" tem que ser um elemento de vídeo real do arquivo anexado
151
  (nunca um `refId` de um elemento que você acabou de criar). "instruction"
152
  é o pedido do usuário sobre COMO cortar, repassado da forma mais fiel
153
+ possível. Essa ação é DIFERENTE da maioria das outras: não é aplicada na
154
  hora — o editor ainda vai transcrever a fala e analisar o que manter,
155
  o que pode levar cerca de um minuto. Por isso, quando usar essa ação,
156
  seu "reply" deve deixar isso claro (ex: "Vou ouvir o vídeo e separar as
157
  melhores falas — isso pode levar um minuto."), nunca afirmar que o corte
158
+ já foi feito.
159
+ 11. analyze_media — {{ "type": "analyze_media", "elementId": string, "instruction": string }}
160
+ Use quando o pedido for pra SABER/DESCREVER algo sobre um elemento de
161
+ vídeo (type "image" com mediaType "video") ou de ÁUDIO (type "audio") do
162
+ arquivo anexado — algo que só dá pra responder de verdade assistindo/
163
+ ouvindo o clipe: descrever a cena, quem aparece, o que está vestindo, o
164
+ ambiente, o tom/clima, a qualidade de imagem/áudio, música de fundo,
165
+ efeitos sonoros, texto que aparece na tela, resumir o que foi dito e
166
+ como, etc. Ex: "descreve esse vídeo pra mim", "o que essa pessoa está
167
+ falando e qual o tom dela", "esse áudio tem música de fundo?". Essa ação
168
+ é SÓ INFORMATIVA — nunca edita o documento. "elementId" tem que ser um
169
+ elemento de vídeo ou áudio real do arquivo anexado com `hasMedia: true`
170
+ (nunca um `refId` de algo que você acabou de criar). "instruction" é a
171
+ pergunta/pedido do usuário sobre esse clipe, repassado da forma mais
172
+ fiel possível. Igual smart_cut_video, essa ação NÃO é aplicada na hora —
173
+ o editor ainda vai assistir/ouvir o arquivo de verdade, o que pode levar
174
+ cerca de um minuto. Seu "reply" deve deixar isso claro (ex: "Vou
175
+ assistir/ouvir com atenção e te conto os detalhes — pode levar um
176
+ tempinho."), nunca afirmar que a análise já foi feita nem inventar o que
177
+ tem no vídeo/áudio antes de ela realmente rodar.
178
+
179
+ No máximo UMA ação assíncrona (smart_cut_video OU analyze_media, nunca as
180
+ duas na mesma resposta) por resposta — são as únicas ações que não são
181
+ aplicadas na hora.
182
 
183
  TEMPLATES DISPONÍVEIS (use o "id" exato em insert_template.templateId; os ids
184
  dentro de "editableFields" podem ser usados como chave em "overrides"):
 
219
  de elemento, alterar o código/comportamento do próprio editor, acessar
220
  informação de fora do que foi anexado). Cortar vídeo com base na fala JÁ é
221
  possível — ver ação 10 (smart_cut_video) — não trate isso como limitação.
222
+ Descrever/analisar o conteúdo real de um vídeo ou áudio (o que aparece, o
223
+ que é dito, o clima, a qualidade) TAMBÉM já é possível — ver ação 11
224
+ (analyze_media) — não trate isso como limitação.
225
  Se o usuário pedir algo fora do catálogo, ou qualquer coisa
226
  que você não tenha certeza de conseguir fazer com o catálogo, diga isso de
227
  forma CLARA, HONESTA e EDUCADA no "reply" — algo como "ainda não fui
 
469
  except Exception as e:
470
  print(f"❌ Erro no /editor/smart-cut: {e}")
471
  raise HTTPException(status_code=500, detail=str(e))
472
+ finally:
473
+ if original_media_to_delete and os.path.exists(original_media_to_delete) and "static" in original_media_to_delete:
474
+ try:
475
+ os.unlink(original_media_to_delete)
476
+ except Exception:
477
+ pass
478
+ if context_path and os.path.exists(context_path):
479
+ try:
480
+ os.remove(context_path)
481
+ except Exception:
482
+ pass
483
+
484
+
485
+ # ── Media analysis (analyze_media) ─────────────────────────────────────────
486
+ # Diferente de smart-cut, aqui o Gemini REALMENTE assiste/ouve o arquivo de
487
+ # mídia real (o mesmo `gemini_filepath` que get_groq_srt_base já baixa/recorta
488
+ # e que /smart-cut baixa hoje e descarta sem usar) — não só o texto da
489
+ # transcrição. Groq ainda transcreve (quando há fala), pra dar ao Gemini
490
+ # palavras exatas além do que ele observa, mas a transcrição nunca é
491
+ # obrigatória: um clipe mudo/só música é um resultado válido, e a análise
492
+ # continua útil baseada só no que foi visto/ouvido.
493
+
494
+ def build_analyze_media_prompt(instruction: str, has_transcript: bool) -> str:
495
+ if has_transcript:
496
+ transcript_note = (
497
+ "Uma TRANSCRIÇÃO exata da fala (formato SRT, tempos relativos ao "
498
+ "início do trecho analisado) também está anexada como arquivo de "
499
+ "texto — use-a pra citar palavras exatas, mas sua análise NÃO deve "
500
+ "se limitar a ela: o outro arquivo anexado é o vídeo/áudio real, e "
501
+ "você deve efetivamente assisti-lo/ouvi-lo pra descrever o que NÃO "
502
+ "está na transcrição (quem aparece, cenário, ações, expressões, tom "
503
+ "de voz, qualidade de imagem/áudio, música de fundo, efeitos "
504
+ "sonoros, texto na tela, etc)."
505
+ )
506
+ else:
507
+ transcript_note = (
508
+ "Não foi possível transcrever nenhuma fala nesse trecho (silêncio, "
509
+ "sem diálogo, ou só música/ruído) — baseie sua análise inteiramente "
510
+ "no que você vê e ouve no arquivo de mídia anexado."
511
+ )
512
+
513
+ return f"""
514
+ Você é um editor de vídeo profissional analisando um clipe de vídeo ou áudio
515
+ REAL, anexado a esta mensagem — assista/ouça o arquivo anexado com atenção,
516
+ frame a frame quando relevante, antes de responder.
517
+
518
+ {transcript_note}
519
+
520
+ PEDIDO DO USUÁRIO SOBRE ESSE CLIPE:
521
+ "{instruction}"
522
+
523
+ SUA TAREFA: responder ao pedido do usuário com o máximo de detalhe e precisão
524
+ possível, cobrindo tudo que for relevante que você observou no arquivo real
525
+ (visual e/ou sonoro) — não generalize nem invente o que não conseguir
526
+ observar; se algo não for possível determinar a partir do arquivo, diga isso
527
+ em vez de adivinhar.
528
+
529
+ Responda ESTRITAMENTE em JSON, sem markdown, sem texto fora do JSON:
530
+ {{
531
+ "analysis": "sua análise/resposta em texto normal, natural, pro usuário — pode ser longa se o pedido pedir detalhe"
532
+ }}
533
+
534
+ Mande APENAS o JSON.
535
+ """
536
+
537
+
538
+ @router.post("/analyze-media")
539
+ async def editor_analyze_media(request: AnalyzeMediaRequest):
540
+ if not hasattr(g, "client") or not g.client:
541
+ raise HTTPException(status_code=500, detail="Gemini client is not initialized")
542
+ if request.window_end <= request.window_start:
543
+ raise HTTPException(status_code=400, detail="Invalid time window.")
544
+
545
+ original_media_to_delete = None
546
+ context_path = None
547
+ try:
548
+ print(f"🔍 [ANALYZE-MEDIA] media_url={request.media_url} | janela=[{request.window_start:.2f}, {request.window_end:.2f}] | instrução={request.instruction!r}")
549
+
550
+ srt_base, _, processed_audio_url, _word_level, gemini_filepath = await get_groq_srt_base(
551
+ request.media_url, time_start=request.window_start, time_end=request.window_end,
552
+ )
553
+ original_media_to_delete = gemini_filepath
554
+
555
+ # Same dropped-path quirk noted in /smart-cut above — corrected only
556
+ # for this log line.
557
+ audio_log_url = processed_audio_url.replace("/static/", "/static/processed/", 1) if processed_audio_url else None
558
+ print(f"🔊 [ANALYZE-MEDIA] Áudio processado (o que o Whisper de fato ouviu): {audio_log_url}")
559
+
560
+ # Unlike /smart-cut, transcrição vazia NÃO aborta o pedido — um clipe
561
+ # mudo/só música ainda é um alvo válido de análise visual/sonora.
562
+ has_transcript = bool(srt_base and srt_base.strip())
563
+ print(f"🎙️ [ANALYZE-MEDIA] Transcrição ({'com' if has_transcript else 'sem'} fala reconhecida):\n{srt_base}")
564
+
565
+ files_to_attach = [gemini_filepath]
566
+ if has_transcript:
567
+ temp_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static", "temp")
568
+ os.makedirs(temp_dir, exist_ok=True)
569
+ context_path = os.path.join(temp_dir, f"analyze_media_transcript_{uuid.uuid4().hex[:8]}.srt")
570
+ with open(context_path, "w", encoding="utf-8") as f:
571
+ f.write(srt_base)
572
+ files_to_attach.append(context_path)
573
+
574
+ print(f"🤖 [ANALYZE-MEDIA] Pedindo pro Gemini assistir/ouvir e analisar (arquivos anexados: {len(files_to_attach)})...")
575
+ prompt = build_analyze_media_prompt(request.instruction, has_transcript)
576
+ model_obj = get_gemini_model("flash")
577
+ response = await g.client.generate_content(prompt, files=files_to_attach, model=model_obj)
578
+ print(f"🤖 [ANALYZE-MEDIA] Resposta bruta do Gemini:\n{response.text}")
579
+
580
+ data = extract_json_from_text(response.text)
581
+ if not data or not isinstance(data, dict) or not isinstance(data.get("analysis"), str):
582
+ print("⚠️ [ANALYZE-MEDIA] Resposta do Gemini não veio em JSON válido com 'analysis' — devolvendo o texto bruto.")
583
+ return {"analysis": response.text.strip() or "Não consegui analisar esse trecho — tente de novo."}
584
+
585
+ print(f"📦 [ANALYZE-MEDIA] Análise devolvida ({len(data['analysis'])} caractere(s)).")
586
+ return {"analysis": data["analysis"]}
587
+
588
+ except Exception as e:
589
+ print(f"❌ Erro no /editor/analyze-media: {e}")
590
+ raise HTTPException(status_code=500, detail=str(e))
591
  finally:
592
  if original_media_to_delete and os.path.exists(original_media_to_delete) and "static" in original_media_to_delete:
593
  try: