leicam commited on
Commit
f248bc7
·
verified ·
1 Parent(s): c26c6b6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +33 -299
app.py CHANGED
@@ -98,7 +98,6 @@ def keyword_score(text: str, custom_keywords: str = "", weight_emotion: float =
98
  for kw in kw_learn: score += weight_learn if kw in t else 0.0
99
  for kw in kw_viral: score += weight_viral if kw in t else 0.0
100
 
101
- # Custom keywords
102
  if custom_keywords.strip():
103
  custom_kw_list = [kw.strip().lower() for kw in custom_keywords.split(",") if kw.strip()]
104
  for kw in custom_kw_list:
@@ -109,7 +108,6 @@ def keyword_score(text: str, custom_keywords: str = "", weight_emotion: float =
109
  return score
110
 
111
  def parse_manual_timecodes(manual_input: str) -> List[tuple]:
112
- """Parse manual timecode ranges from user input."""
113
  manual_ranges = []
114
  normalized = manual_input.replace(",", "\n")
115
  lines = [l.strip() for l in normalized.splitlines() if l.strip()]
@@ -125,7 +123,6 @@ def parse_manual_timecodes(manual_input: str) -> List[tuple]:
125
  return manual_ranges
126
 
127
  def llm_process_natural_instructions(transcript_txt: str, natural_instructions: str, num_segments: int) -> List[Segment]:
128
- """Use LLM to interpret natural language instructions and select segments."""
129
  if not LLM_AVAILABLE:
130
  raise ValueError("LLM não disponível. Configure GEMINI_API_KEY para usar instruções em linguagem natural.")
131
 
@@ -133,7 +130,6 @@ def llm_process_natural_instructions(transcript_txt: str, natural_instructions:
133
  if not segs:
134
  raise ValueError("Nenhum trecho válido encontrado na transcrição.")
135
 
136
- # Format segments for LLM
137
  segments_text = "\n".join([
138
  f"{i}. [{s.start_tc} - {s.end_tc}] {s.text}"
139
  for i, s in enumerate(segs)
@@ -148,7 +144,7 @@ TRANSCRIÇÃO COM TIMECODES:
148
  {segments_text}
149
 
150
  TAREFA:
151
- 1. Interprete as instruções do usuário (ex: "separe os melhores momentos", "recorte só a parte sobre medo", "remova quando fala almondega")
152
  2. Selecione os {num_segments} trechos que melhor atendem às instruções
153
  3. Se a instrução for para REMOVER algo, selecione os trechos que NÃO contêm aquilo
154
  4. Se a instrução for para INCLUIR algo específico, selecione apenas os trechos que contêm aquilo
@@ -161,7 +157,6 @@ Não adicione explicações, apenas os números."""
161
  response = LLM.generate_content(prompt, generation_config={"temperature": 0.3})
162
  txt = (response.text or "").strip()
163
 
164
- # Extract indices
165
  idxs = [int(x) for x in re.findall(r"\d+", txt)]
166
  idxs = [i for i in idxs if 0 <= i < len(segs)]
167
 
@@ -169,8 +164,6 @@ Não adicione explicações, apenas os números."""
169
  raise ValueError("LLM não retornou índices válidos")
170
 
171
  selected = [segs[i] for i in idxs[:num_segments]]
172
-
173
- # Sort by timeline order
174
  selected.sort(key=lambda x: x.start_f)
175
 
176
  return selected
@@ -179,7 +172,6 @@ Não adicione explicações, apenas os números."""
179
  raise ValueError(f"Erro ao processar instruções com LLM: {e}")
180
 
181
  def llm_rank_segments(candidates: List[Segment], num_segments: int, custom_instructions: str = "") -> List[Segment]:
182
- """Ask the LLM to pick segments based on criteria."""
183
  if not LLM_AVAILABLE:
184
  return candidates[:num_segments]
185
 
@@ -214,7 +206,6 @@ def select_segments(transcript_txt: str, use_llm: bool, num_segments: int,
214
  weight_emotion: float, weight_break: float,
215
  weight_learn: float, weight_viral: float) -> List[Segment]:
216
 
217
- # Priority 1: Manual timecodes
218
  manual_ranges = parse_manual_timecodes(manual_timecodes)
219
  if manual_ranges:
220
  result_segs = []
@@ -240,11 +231,9 @@ def select_segments(transcript_txt: str, use_llm: bool, num_segments: int,
240
 
241
  return result_segs
242
 
243
- # Priority 2: Natural language instructions with LLM
244
  if natural_instructions.strip() and use_llm and LLM_AVAILABLE:
245
  return llm_process_natural_instructions(transcript_txt, natural_instructions, num_segments)
246
 
247
- # Priority 3: Automatic mode with scoring
248
  segs = parse_transcript(transcript_txt)
249
  if not segs:
250
  raise ValueError("Nenhum trecho válido encontrado na transcrição.")
@@ -337,7 +326,7 @@ def edit_sequence_with_segments(tree: ET.ElementTree, segs: List[Segment]) -> ET
337
  audio_track = seq.find("./media/audio/track")
338
 
339
  if video_track is None or audio_track is None:
340
- raise ValueError("Estrutura de trilhas não encontrada (esperado media/video/track e media/audio/track).")
341
 
342
  v_tpl = first_clipitem_ref(video_track)
343
  a_tpl = first_clipitem_ref(audio_track)
@@ -373,11 +362,9 @@ def process_xml_and_transcript(premiere_xml_file, transcript_txt_file, use_llm,
373
  if premiere_xml_file is None:
374
  return "Envie o XML do Premiere.", None, f"LLM disponível: {LLM_AVAILABLE}"
375
 
376
- # Check priorities
377
  manual_ranges = parse_manual_timecodes(manual_timecodes)
378
  has_natural_instructions = natural_instructions.strip() != ""
379
 
380
- # Determine mode
381
  if manual_ranges:
382
  mode = "MANUAL"
383
  transcript = ""
@@ -419,274 +406,100 @@ def process_xml_and_transcript(premiere_xml_file, transcript_txt_file, use_llm,
419
  return resumo, out_path, status
420
 
421
  css = """
422
- /* Design Tokens */
423
  :root {
424
  --neon: #39FF14;
425
  --txt: #1a1a1a;
426
  --muted: #4b5563;
427
  --line: #d1d5db;
428
- --bg: #ffffff;
429
  }
430
 
431
- /* Global Styles */
432
  .gradio-container {
433
- font-family: 'Manrope', system-ui, -apple-system, sans-serif !important;
434
- background: linear-gradient(135deg, rgba(57,255,20,0.03) 0%, rgba(255,255,255,1) 100%);
435
- background-attachment: fixed;
436
  }
437
 
438
- /* Headers */
439
- .gradio-container h1, .gradio-container h2, .gradio-container h3 {
440
- font-weight: 800 !important;
441
- letter-spacing: -0.3px !important;
442
  color: var(--txt) !important;
 
443
  }
444
 
445
- .gradio-container h1 {
446
- font-size: clamp(28px, 5vw, 46px) !important;
447
- margin-bottom: 8px !important;
448
- }
449
-
450
- .gradio-container .gr-prose p {
451
- color: var(--muted) !important;
452
- line-height: 1.65 !important;
453
- font-size: 16px !important;
454
- }
455
-
456
- /* Labels */
457
- .gradio-container label {
458
- color: var(--txt) !important;
459
- font-weight: 600 !important;
460
- }
461
-
462
- /* Buttons */
463
  .gradio-container button.primary {
464
  background: var(--neon) !important;
465
  color: #000 !important;
466
- border: none !important;
467
- border-radius: 10px !important;
468
  font-weight: 800 !important;
469
- padding: 12px 20px !important;
470
- box-shadow: 0 2px 0 rgba(0,0,0,0.12), 0 10px 30px rgba(57,255,20,0.18) !important;
471
- transition: all 0.2s ease !important;
472
- }
473
-
474
- .gradio-container button.primary:hover {
475
- transform: translateY(-1px) !important;
476
- filter: saturate(1.03) !important;
477
- }
478
-
479
- .gradio-container button:not(.primary) {
480
- background: #fff !important;
481
- border: 1px solid var(--line) !important;
482
  border-radius: 10px !important;
483
- color: var(--txt) !important;
484
- font-weight: 600 !important;
485
  }
486
 
487
- /* Inputs, Textareas, File uploads */
488
- .gradio-container input, .gradio-container textarea, .gradio-container .wrap {
489
- border: 1px solid var(--line) !important;
490
- border-radius: 12px !important;
491
- background: #fff !important;
492
- transition: all 0.2s ease !important;
493
- color: var(--txt) !important;
494
- }
495
-
496
- .gradio-container input:focus, .gradio-container textarea:focus {
497
- border-color: #cbd5e1 !important;
498
- box-shadow: 0 0 0 3px rgba(57,255,20,0.16) !important;
499
- }
500
-
501
- /* Cards/Panels */
502
- .gradio-container .block {
503
- border: 1px solid var(--line) !important;
504
- border-radius: 16px !important;
505
- background: #fff !important;
506
- box-shadow: 0 2px 8px rgba(0,0,0,0.06) !important;
507
- transition: all 0.2s ease !important;
508
- }
509
-
510
- .gradio-container .block:hover {
511
- box-shadow: 0 6px 16px rgba(0,0,0,0.08) !important;
512
- transform: translateY(-1px) !important;
513
- }
514
-
515
- /* Accordion */
516
- .gradio-container .label-wrap {
517
- font-weight: 700 !important;
518
  color: var(--txt) !important;
 
519
  }
520
 
521
- /* Checkboxes */
522
  .gradio-container input[type="checkbox"]:checked {
523
  background: var(--neon) !important;
524
- border-color: var(--neon) !important;
525
- }
526
-
527
- /* Sliders */
528
- .gradio-container input[type="range"]::-webkit-slider-thumb {
529
- background: var(--neon) !important;
530
- }
531
-
532
- .gradio-container input[type="range"]::-moz-range-thumb {
533
- background: var(--neon) !important;
534
- }
535
-
536
- /* File upload areas */
537
- .gradio-container .upload-container {
538
- border: 2px dashed var(--line) !important;
539
- border-radius: 12px !important;
540
- background: #fafafa !important;
541
- }
542
-
543
- /* Info text */
544
- .gradio-container .gr-form-info {
545
- color: var(--muted) !important;
546
- }
547
-
548
- /* Textbox content */
549
- .gradio-container textarea {
550
- color: var(--txt) !important;
551
  }
552
  """
553
 
554
  with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
555
  gr.HTML("""
556
  <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&display=swap" rel="stylesheet">
557
- <div style="text-align: center; padding: 24px 0 16px;">
558
- <div style="display: inline-flex; align-items: center; gap: 8px; margin-bottom: 12px;">
559
- <div style="width: 12px; height: 12px; border-radius: 50%; background: #39FF14; box-shadow: 0 0 20px rgba(57,255,20,0.4);"></div>
560
- <h1 style="margin: 0; font-weight: 800; letter-spacing: -0.4px; color: #1a1a1a;">Agente de Edição XML · Premiere</h1>
561
- </div>
562
- <p style="color: #4b5563; max-width: 720px; margin: 0 auto; line-height: 1.65;">
563
- Edite sua sequência do Premiere com <strong>controle total</strong> sobre a seleção de trechos.
564
- Use linguagem natural, modo manual ou automático com IA.
565
- </p>
566
  </div>
567
  """)
568
 
569
  with gr.Row():
570
  with gr.Column():
571
- gr.HTML("""<div style="background: linear-gradient(135deg, #f9fafb 0%, #fff 100%);
572
- padding: 16px; border-radius: 16px; border: 1px solid #e5e7eb; margin-bottom: 16px;">
573
- <div style="font-weight: 700; color: #1a1a1a; margin-bottom: 8px;"> Arquivos de entrada</div>
574
- <p style="color: #4b5563; font-size: 14px; margin: 0;">Envie o XML exportado do Premiere e a transcrição</p>
575
- </div>""")
576
  xml_in = gr.File(label="XML da sequência (FCP XML)", file_types=[".xml"])
577
- txt_in = gr.File(label="Transcrição (.txt) - Opcional no modo manual", file_types=[".txt"])
578
 
579
  with gr.Column():
580
- gr.HTML("""<div style="background: linear-gradient(135deg, rgba(57,255,20,0.08) 0%, rgba(57,255,20,0.02) 100%);
581
- padding: 16px; border-radius: 16px; border: 1px solid #e5e7eb; margin-bottom: 16px;">
582
- <div style="font-weight: 700; color: #1a1a1a; margin-bottom: 8px;"> Configurações básicas</div>
583
- <p style="color: #4b5563; font-size: 14px; margin: 0;">Ajuste o comportamento do processamento</p>
584
- </div>""")
585
  use_llm = gr.Checkbox(
586
- label=" Usar Potência Criativa (IA)",
587
- value=USE_LLM_DEFAULT and LLM_AVAILABLE,
588
- info="Usa IA para escolher os melhores trechos narrativamente"
589
  )
590
  num_segments = gr.Slider(
591
  minimum=2, maximum=10, step=1, value=5,
592
- label="Número de segmentos",
593
- info="Quantos trechos incluir no vídeo final"
594
  )
595
 
596
- with gr.Accordion(" INSTRUÇÕES EM LINGUAGEM NATURAL (IA) - NOVO!", open=True):
597
- gr.HTML("""<div style="background: linear-gradient(135deg, #dbeafe 0%, #eff6ff 100%);
598
- padding: 14px; border-radius: 12px; border: 1px solid #93c5fd; margin-bottom: 12px;">
599
- <strong style="color: #1e3a8a;"> Fale naturalmente com a IA!</strong>
600
- <p style="color: #1e40af; font-size: 13px; margin: 6px 0 0; line-height: 1.6;">
601
- Descreva o que você quer em texto simples. A IA vai interpretar e selecionar os trechos certos.
602
- <br><strong>Prioridade:</strong> Se preencher este campo, ele tem preferência sobre palavras-chave e pesos.
603
- </p>
604
- </div>""")
605
  natural_instructions = gr.Textbox(
606
  label="Suas instruções para a IA",
607
- placeholder="""Exemplos:
608
- • "Separe os 5 melhores momentos desse vídeo"
609
- • "Recorte apenas a parte que ele fala sobre medo e superação"
610
- • "Remova todas as vezes que ele menciona a palavra 'almôndega'"
611
- • "Quero só os momentos engraçados e emocionantes"
612
- • "Extraia as partes onde ele ensina alguma técnica"
613
- • "Pegue os trechos mais virais para o TikTok",
614
- lines=4,
615
- info="Requer LLM ativo e transcrição. Tem prioridade sobre modo automático."
616
  )
617
- gr.HTML("""
618
- <div style="padding: 12px; background: #f0f9ff; border-radius: 10px; margin-top: 12px; border: 1px solid #bae6fd;">
619
- <div style="font-weight: 600; margin-bottom: 8px; color: #0c4a6e;"> Como funciona:</div>
620
- <ul style="margin: 0; padding-left: 20px; color: #0c4a6e; font-size: 13px; line-height: 1.7;">
621
- <li>A IA lê toda a transcrição com timecodes</li>
622
- <li>Interpreta suas instruções em português natural</li>
623
- <li>Seleciona automaticamente os trechos que atendem ao seu pedido</li>
624
- <li>Funciona para incluir, excluir ou filtrar conteúdo específico</li>
625
- </ul>
626
- </div>
627
- """)
628
 
629
- with gr.Accordion(" MINUTAGENS MANUAIS (Controle Absoluto)", open=False):
630
- gr.HTML("""<div style="background: #fffbeb; padding: 12px; border-radius: 10px; border: 1px solid #fde68a; margin-bottom: 12px;">
631
- <strong style="color: #92400e;"> Modo de Controle Total</strong>
632
- <p style="color: #78350f; font-size: 13px; margin: 6px 0 0;">
633
- Se preencher este campo, o app ignora TUDO (transcrição, IA, instruções, etc)
634
- e corta EXATAMENTE o que você especificou.
635
- </p>
636
- </div>""")
637
  manual_timecodes = gr.Textbox(
638
- label="Cole aqui os timecodes exatos que você quer cortar",
639
- placeholder="Exemplo:\n00:01:23:15 - 00:02:45:10\n00:05:30:00 - 00:07:15:22\n00:10:00:05 - 00:12:30:18",
640
  lines=5
641
  )
642
- gr.HTML("""
643
- <div style="padding: 12px; background: #f9fafb; border-radius: 10px; margin-top: 12px;">
644
- <div style="font-weight: 600; margin-bottom: 8px; color: #1a1a1a;">Formatos aceitos:</div>
645
- <ul style="margin: 0; padding-left: 20px; color: #374151; font-size: 13px;">
646
- <li><code>hh:mm:ss:ff - hh:mm:ss:ff</code> (um por linha)</li>
647
- <li>Pode separar por vírgula também</li>
648
- </ul>
649
- </div>
650
- """)
651
 
652
- with gr.Accordion(" Palavras-chave Personalizadas (Modo Automático)", open=False):
653
  custom_keywords = gr.Textbox(
654
- label="Adicione palavras-chave importantes (separadas por vírgula)",
655
- placeholder="Exemplo: transformação, resultado, método, estratégia",
656
- info="Trechos com essas palavras terão prioridade máxima (peso 3.0) - Só funciona no modo automático"
657
  )
658
 
659
- with gr.Accordion(" Ajuste Fino dos Pesos de Pontuação (Modo Automático)", open=False):
660
- gr.HTML("""<p style="color: #4b5563; margin-bottom: 16px;">
661
- Ajuste a importância de cada categoria na pontuação heurística (modo automático)</p>""")
662
  with gr.Row():
663
- weight_emotion = gr.Slider(0, 5, value=2.0, step=0.1, label=" Emoção")
664
- weight_break = gr.Slider(0, 5, value=1.5, step=0.1, label=" Quebra de expectativa")
665
  with gr.Row():
666
- weight_learn = gr.Slider(0, 5, value=1.2, step=0.1, label=" Aprendizado")
667
- weight_viral = gr.Slider(0, 5, value=1.0, step=0.1, label=" Viralização")
668
-
669
- gr.HTML("""<div style="margin: 24px 0; text-align: center;">
670
- <hr style="border: none; border-top: 1px solid #e5e7eb; margin: 16px 0;">
671
- </div>""")
672
 
673
- run_btn = gr.Button(" Processar e Gerar XML Editado", variant="primary", size="lg")
674
-
675
- gr.HTML("""<div style="margin: 20px 0;">
676
- <hr style="border: none; border-top: 1px solid #e5e7eb;">
677
- </div>""")
678
-
679
- gr.HTML("""<div style="text-align: center; margin-bottom: 16px;">
680
- <div style="font-weight: 700; color: #1a1a1a; margin-bottom: 4px;"> Resultados</div>
681
- <p style="color: #4b5563; font-size: 14px; margin: 0;">Resumo dos cortes e arquivo para download</p>
682
- </div>""")
683
 
684
  with gr.Row():
685
  with gr.Column(scale=2):
686
- resumo_out = gr.Textbox(label="Resumo dos cortes aplicados", lines=15)
687
  with gr.Column(scale=1):
688
- status_out = gr.Textbox(label="Status", interactive=False)
689
- file_out = gr.File(label="Download do XML Editado")
690
 
691
  run_btn.click(
692
  process_xml_and_transcript,
@@ -694,85 +507,6 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
694
  manual_timecodes, natural_instructions, weight_emotion, weight_break, weight_learn, weight_viral],
695
  outputs=[resumo_out, file_out, status_out]
696
  )
697
-
698
- with gr.Accordion(" Guia de Uso Completo", open=False):
699
- gr.HTML("""
700
- <div style="padding: 16px; background: #f9fafb; border-radius: 12px;">
701
- <div style="margin-bottom: 24px;">
702
- <div style="display: inline-block; background: #3b82f6; color: #fff; padding: 6px 12px;"
703
- border-radius: "999px; font-size: 12px; font-weight: 900; margin-bottom: 10px;">
704
- MODO 1: INSTRUÇÕES NATURAIS (NOVO!)
705
- </div>
706
- <h3 style="margin: 8px 0; font-weight: 700; color: #1a1a1a;">Fale com a IA</h3>
707
- <ul style="color: #374151; line-height: 1.8; padding-left: 20px;">
708
- <li><strong>Prioridade:</strong> Se preencher as instruções naturais, ignora palavras-chave e pesos</li>
709
- <li>Escreva em português simples o que você quer</li>
710
- <li>Exemplos: "separe os melhores momentos", "só a parte sobre medo", "remova quando fala X"</li>
711
- <li>Requer LLM ativo e transcrição com timecodes</li>
712
- <li>A IA interpreta e seleciona automaticamente</li>
713
- </ul>
714
- <div style="background: #e0f2fe; padding: 10px; border-radius: 8px; margin-top: 10px; border-left: 3px solid #0284c7;">
715
- <strong style="color: #0c4a6e;"> Exemplos de comandos:</strong><br>
716
- <code style="color: #0369a1; font-size: 12px;">
717
- • "Pegue só as partes emocionantes"<br>
718
- • "Remova tudo que fala sobre política"<br>
719
- • "Extraia os 3 melhores ensinamentos"<br>
720
- • "Corte apenas os momentos engraçados"
721
- </code>
722
- </div>
723
- </div>
724
-
725
- <div style="margin-bottom: 24px;">
726
- <div style="display: inline-block; background: #fbbf24; color: #000; padding: 6px 12px;"
727
- border-radius: "999px; font-size: 12px; font-weight: 900; margin-bottom: 10px;">
728
- MODO 2: MINUTAGENS MANUAIS
729
- </div>
730
- <h3 style="margin: 8px 0; font-weight: 700; color: #1a1a1a;">Controle Absoluto</h3>
731
- <ul style="color: #374151; line-height: 1.8; padding-left: 20px;">
732
- <li><strong>Máxima prioridade:</strong> Sobrescreve TUDO (IA, transcrição, etc)</li>
733
- <li>Cole os timecodes exatos no formato <code>hh:mm:ss:ff - hh:mm:ss:ff</code></li>
734
- <li>A transcrição se torna opcional</li>
735
- <li>Perfeito quando você já sabe exatamente o que quer</li>
736
- </ul>
737
- </div>
738
-
739
- <div style="margin-bottom: 24px;">
740
- <div style="display: inline-block; background: #39FF14; color: #000; padding: 6px 12px;"
741
- border-radius: "999px; font-size: 12px; font-weight: 900; margin-bottom: 10px;">
742
- MODO 3: AUTOMÁTICO COM IA
743
- </div>
744
- <h3 style="margin: 8px 0; font-weight: 700; color: #1a1a1a;">Seleção Inteligente</h3>
745
- <ul style="color: #374151; line-height: 1.8; padding-left: 20px;">
746
- <li>Deixe instruções naturais e minutagens vazias</li>
747
- <li>Envie a transcrição com timecodes</li>
748
- <li>Ative "Usar Potência Criativa" para melhor seleção narrativa</li>
749
- <li>Configure palavras-chave personalizadas se quiser</li>
750
- <li>Ajuste os pesos de pontuação (opcional)</li>
751
- </ul>
752
- </div>
753
-
754
- <div style="background: #fef3c7; padding: 14px; border-radius: 10px; border: 1px solid #fcd34d; margin-top: 20px;">
755
- <strong style="color: #78350f;"> Ordem de Prioridade:</strong>
756
- <ol style="margin: 8px 0 0; padding-left: 20px; color: #92400e; line-height: 1.7;">
757
- <li><strong>Minutagens Manuais</strong> - Ignora tudo</li>
758
- <li><strong>Instruções Naturais com IA</strong> - Ignora palavras-chave e pesos</li>
759
- <li><strong>Modo Automático</strong> - Usa pontuação + palavras-chave + IA (se ativada)</li>
760
- </ol>
761
- </div>
762
- </div>
763
- """)
764
-
765
- gr.HTML("""
766
- <footer style="margin-top: 40px; padding: 24px 0; border-top: 1px solid #e5e7eb; text-align: center;">
767
- <div style="display: inline-flex; align-items: center; gap: 8px; margin-bottom: 8px;">
768
- <div style="width: 10px; height: 10px; border-radius: 50%; background: #39FF14;"></div>
769
- <span style="font-weight: 700; color: #1a1a1a;">Leicam · Tech</span>
770
- </div>
771
- <p style="color: #6b7280; font-size: 13px; margin: 0;">
772
- Ferramentas práticas para produção de conteúdo
773
- </p>
774
- </footer>
775
- """)
776
 
777
  if __name__ == "__main__":
778
  demo.launch()
 
98
  for kw in kw_learn: score += weight_learn if kw in t else 0.0
99
  for kw in kw_viral: score += weight_viral if kw in t else 0.0
100
 
 
101
  if custom_keywords.strip():
102
  custom_kw_list = [kw.strip().lower() for kw in custom_keywords.split(",") if kw.strip()]
103
  for kw in custom_kw_list:
 
108
  return score
109
 
110
  def parse_manual_timecodes(manual_input: str) -> List[tuple]:
 
111
  manual_ranges = []
112
  normalized = manual_input.replace(",", "\n")
113
  lines = [l.strip() for l in normalized.splitlines() if l.strip()]
 
123
  return manual_ranges
124
 
125
  def llm_process_natural_instructions(transcript_txt: str, natural_instructions: str, num_segments: int) -> List[Segment]:
 
126
  if not LLM_AVAILABLE:
127
  raise ValueError("LLM não disponível. Configure GEMINI_API_KEY para usar instruções em linguagem natural.")
128
 
 
130
  if not segs:
131
  raise ValueError("Nenhum trecho válido encontrado na transcrição.")
132
 
 
133
  segments_text = "\n".join([
134
  f"{i}. [{s.start_tc} - {s.end_tc}] {s.text}"
135
  for i, s in enumerate(segs)
 
144
  {segments_text}
145
 
146
  TAREFA:
147
+ 1. Interprete as instruções do usuário
148
  2. Selecione os {num_segments} trechos que melhor atendem às instruções
149
  3. Se a instrução for para REMOVER algo, selecione os trechos que NÃO contêm aquilo
150
  4. Se a instrução for para INCLUIR algo específico, selecione apenas os trechos que contêm aquilo
 
157
  response = LLM.generate_content(prompt, generation_config={"temperature": 0.3})
158
  txt = (response.text or "").strip()
159
 
 
160
  idxs = [int(x) for x in re.findall(r"\d+", txt)]
161
  idxs = [i for i in idxs if 0 <= i < len(segs)]
162
 
 
164
  raise ValueError("LLM não retornou índices válidos")
165
 
166
  selected = [segs[i] for i in idxs[:num_segments]]
 
 
167
  selected.sort(key=lambda x: x.start_f)
168
 
169
  return selected
 
172
  raise ValueError(f"Erro ao processar instruções com LLM: {e}")
173
 
174
  def llm_rank_segments(candidates: List[Segment], num_segments: int, custom_instructions: str = "") -> List[Segment]:
 
175
  if not LLM_AVAILABLE:
176
  return candidates[:num_segments]
177
 
 
206
  weight_emotion: float, weight_break: float,
207
  weight_learn: float, weight_viral: float) -> List[Segment]:
208
 
 
209
  manual_ranges = parse_manual_timecodes(manual_timecodes)
210
  if manual_ranges:
211
  result_segs = []
 
231
 
232
  return result_segs
233
 
 
234
  if natural_instructions.strip() and use_llm and LLM_AVAILABLE:
235
  return llm_process_natural_instructions(transcript_txt, natural_instructions, num_segments)
236
 
 
237
  segs = parse_transcript(transcript_txt)
238
  if not segs:
239
  raise ValueError("Nenhum trecho válido encontrado na transcrição.")
 
326
  audio_track = seq.find("./media/audio/track")
327
 
328
  if video_track is None or audio_track is None:
329
+ raise ValueError("Estrutura de trilhas não encontrada.")
330
 
331
  v_tpl = first_clipitem_ref(video_track)
332
  a_tpl = first_clipitem_ref(audio_track)
 
362
  if premiere_xml_file is None:
363
  return "Envie o XML do Premiere.", None, f"LLM disponível: {LLM_AVAILABLE}"
364
 
 
365
  manual_ranges = parse_manual_timecodes(manual_timecodes)
366
  has_natural_instructions = natural_instructions.strip() != ""
367
 
 
368
  if manual_ranges:
369
  mode = "MANUAL"
370
  transcript = ""
 
406
  return resumo, out_path, status
407
 
408
  css = """
 
409
  :root {
410
  --neon: #39FF14;
411
  --txt: #1a1a1a;
412
  --muted: #4b5563;
413
  --line: #d1d5db;
 
414
  }
415
 
 
416
  .gradio-container {
417
+ font-family: 'Manrope', system-ui, sans-serif !important;
418
+ background: linear-gradient(135deg, rgba(57,255,20,0.03) 0%, #fff 100%);
 
419
  }
420
 
421
+ .gradio-container h1, .gradio-container h2, .gradio-container h3, .gradio-container label {
 
 
 
422
  color: var(--txt) !important;
423
+ font-weight: 700 !important;
424
  }
425
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
  .gradio-container button.primary {
427
  background: var(--neon) !important;
428
  color: #000 !important;
 
 
429
  font-weight: 800 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  border-radius: 10px !important;
 
 
431
  }
432
 
433
+ .gradio-container input, .gradio-container textarea {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
  color: var(--txt) !important;
435
+ border-radius: 12px !important;
436
  }
437
 
 
438
  .gradio-container input[type="checkbox"]:checked {
439
  background: var(--neon) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  }
441
  """
442
 
443
  with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
444
  gr.HTML("""
445
  <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&display=swap" rel="stylesheet">
446
+ <div style="text-align: center; padding: 24px 0;">
447
+ <h1 style="color: #1a1a1a; font-weight: 800;">Agente de Edição XML · Premiere</h1>
448
+ <p style="color: #4b5563;">Edite sua sequência do Premiere com controle total</p>
 
 
 
 
 
 
449
  </div>
450
  """)
451
 
452
  with gr.Row():
453
  with gr.Column():
 
 
 
 
 
454
  xml_in = gr.File(label="XML da sequência (FCP XML)", file_types=[".xml"])
455
+ txt_in = gr.File(label="Transcrição (.txt)", file_types=[".txt"])
456
 
457
  with gr.Column():
 
 
 
 
 
458
  use_llm = gr.Checkbox(
459
+ label="Usar Potência Criativa (IA)",
460
+ value=USE_LLM_DEFAULT and LLM_AVAILABLE
 
461
  )
462
  num_segments = gr.Slider(
463
  minimum=2, maximum=10, step=1, value=5,
464
+ label="Número de segmentos"
 
465
  )
466
 
467
+ with gr.Accordion("INSTRUÇÕES EM LINGUAGEM NATURAL (IA)", open=True):
 
 
 
 
 
 
 
 
468
  natural_instructions = gr.Textbox(
469
  label="Suas instruções para a IA",
470
+ placeholder='Exemplos:\n"Separe os 5 melhores momentos"\n"Recorte apenas a parte sobre medo"\n"Remova quando fala almôndega"',
471
+ lines=4
 
 
 
 
 
 
 
472
  )
 
 
 
 
 
 
 
 
 
 
 
473
 
474
+ with gr.Accordion("MINUTAGENS MANUAIS", open=False):
 
 
 
 
 
 
 
475
  manual_timecodes = gr.Textbox(
476
+ label="Cole os timecodes exatos",
477
+ placeholder="00:01:23:15 - 00:02:45:10\n00:05:30:00 - 00:07:15:22",
478
  lines=5
479
  )
 
 
 
 
 
 
 
 
 
480
 
481
+ with gr.Accordion("Palavras-chave Personalizadas", open=False):
482
  custom_keywords = gr.Textbox(
483
+ label="Palavras-chave (separadas por vírgula)",
484
+ placeholder="transformação, resultado, método"
 
485
  )
486
 
487
+ with gr.Accordion("Ajuste de Pesos", open=False):
 
 
488
  with gr.Row():
489
+ weight_emotion = gr.Slider(0, 5, value=2.0, step=0.1, label="Emoção")
490
+ weight_break = gr.Slider(0, 5, value=1.5, step=0.1, label="Quebra")
491
  with gr.Row():
492
+ weight_learn = gr.Slider(0, 5, value=1.2, step=0.1, label="Aprendizado")
493
+ weight_viral = gr.Slider(0, 5, value=1.0, step=0.1, label="Viral")
 
 
 
 
494
 
495
+ run_btn = gr.Button("Processar e Gerar XML Editado", variant="primary", size="lg")
 
 
 
 
 
 
 
 
 
496
 
497
  with gr.Row():
498
  with gr.Column(scale=2):
499
+ resumo_out = gr.Textbox(label="Resumo dos cortes", lines=15)
500
  with gr.Column(scale=1):
501
+ status_out = gr.Textbox(label="Status")
502
+ file_out = gr.File(label="Download do XML")
503
 
504
  run_btn.click(
505
  process_xml_and_transcript,
 
507
  manual_timecodes, natural_instructions, weight_emotion, weight_break, weight_learn, weight_viral],
508
  outputs=[resumo_out, file_out, status_out]
509
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
510
 
511
  if __name__ == "__main__":
512
  demo.launch()