#!/usr/bin/env python3 """ Audio Transcriber v1.1.0 Transcreve áudio para texto e gera atas/resumos usando LLM. """ import os import sys import json import subprocess import shutil from datetime import datetime from pathlib import Path # Rich for beautiful terminal output try: from rich.console import Console from rich.prompt import Prompt from rich.panel import Panel from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn from rich import print as rprint RICH_AVAILABLE = True except ImportError: RICH_AVAILABLE = False print("⚠️ Installing rich for better UI...") subprocess.run([sys.executable, "-m", "pip", "install", "--user", "rich"], check=False) from rich.console import Console from rich.prompt import Prompt from rich.panel import Panel from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn from rich import print as rprint # tqdm for progress bars try: from tqdm import tqdm except ImportError: print("⚠️ Installing tqdm for progress bars...") subprocess.run([sys.executable, "-m", "pip", "install", "--user", "tqdm"], check=False) from tqdm import tqdm # Whisper engines try: from faster_whisper import WhisperModel TRANSCRIBER = "faster-whisper" except ImportError: try: import whisper TRANSCRIBER = "whisper" except ImportError: print("❌ Nenhum engine de transcrição encontrado!") print(" Instale: pip install faster-whisper") sys.exit(1) console = Console() # Template padrão RISEN para fallback DEFAULT_MEETING_PROMPT = """ Role: Você é um transcritor profissional especializado em documentação. Instructions: Transforme a transcrição fornecida em um documento estruturado e profissional. Steps: 1. Identifique o tipo de conteúdo (reunião, palestra, entrevista, etc.) 2. Extraia os principais tópicos e pontos-chave 3. Identifique participantes/speakers (se aplicável) 4. Extraia decisões tomadas e ações definidas (se reunião) 5. Organize em formato apropriado com seções claras 6. Use Markdown para formatação profissional End Goal: Documento final bem estruturado, legível e pronto para distribuição. Narrowing: - Mantenha objetividade e clareza - Preserve contexto importante - Use formatação Markdown adequada - Inclua timestamps relevantes quando aplicável """ def detect_cli_tool(): """Detecta qual CLI de LLM está disponível (claude > gh copilot).""" if shutil.which('claude'): return 'claude' elif shutil.which('gh'): result = subprocess.run(['gh', 'copilot', '--version'], capture_output=True, text=True) if result.returncode == 0: return 'gh-copilot' return None def invoke_prompt_engineer(raw_prompt, timeout=90): """ Invoca prompt-engineer skill via CLI para melhorar/gerar prompts. Args: raw_prompt: Prompt a ser melhorado ou meta-prompt timeout: Timeout em segundos Returns: str: Prompt melhorado ou DEFAULT_MEETING_PROMPT se falhar """ try: # Tentar via gh copilot console.print("[dim] Invocando prompt-engineer...[/dim]") result = subprocess.run( ['gh', 'copilot', 'suggest', '-t', 'shell', raw_prompt], capture_output=True, text=True, timeout=timeout ) if result.returncode == 0 and result.stdout.strip(): return result.stdout.strip() else: console.print("[yellow]⚠️ prompt-engineer não respondeu, usando template padrão[/yellow]") return DEFAULT_MEETING_PROMPT except subprocess.TimeoutExpired: console.print(f"[red]⚠️ Timeout após {timeout}s, usando template padrão[/red]") return DEFAULT_MEETING_PROMPT except Exception as e: console.print(f"[red]⚠️ Erro ao invocar prompt-engineer: {e}[/red]") return DEFAULT_MEETING_PROMPT def handle_prompt_workflow(user_prompt, transcript): """ Gerencia fluxo completo de prompts com prompt-engineer. Cenário A: Usuário forneceu prompt → Melhorar AUTOMATICAMENTE → Confirmar Cenário B: Sem prompt → Sugerir tipo → Confirmar → Gerar → Confirmar Returns: str: Prompt final a usar, ou None se usuário recusou processamento """ prompt_engineer_available = os.path.exists( os.path.expanduser('~/.copilot/skills/prompt-engineer/SKILL.md') ) # ========== CENÁRIO A: USUÁRIO FORNECEU PROMPT ========== if user_prompt: console.print("\n[cyan]📝 Prompt fornecido pelo usuário[/cyan]") console.print(Panel(user_prompt[:300] + ("..." if len(user_prompt) > 300 else ""), title="Prompt original", border_style="dim")) if prompt_engineer_available: # Melhora AUTOMATICAMENTE (sem perguntar) console.print("\n[cyan]🔧 Melhorando prompt com prompt-engineer...[/cyan]") improved_prompt = invoke_prompt_engineer( f"melhore este prompt:\n\n{user_prompt}" ) # Mostrar AMBAS versões console.print("\n[green]✨ Versão melhorada:[/green]") console.print(Panel(improved_prompt[:500] + ("..." if len(improved_prompt) > 500 else ""), title="Prompt otimizado", border_style="green")) console.print("\n[dim]📝 Versão original:[/dim]") console.print(Panel(user_prompt[:300] + ("..." if len(user_prompt) > 300 else ""), title="Seu prompt", border_style="dim")) # Pergunta qual usar confirm = Prompt.ask( "\n💡 Usar versão melhorada?", choices=["s", "n"], default="s" ) return improved_prompt if confirm == "s" else user_prompt else: # prompt-engineer não disponível console.print("[yellow]⚠️ prompt-engineer skill não disponível[/yellow]") console.print("[dim]✅ Usando seu prompt original[/dim]") return user_prompt # ========== CENÁRIO B: SEM PROMPT - AUTO-GERAÇÃO ========== else: console.print("\n[yellow]⚠️ Nenhum prompt fornecido.[/yellow]") if not prompt_engineer_available: console.print("[yellow]⚠️ prompt-engineer skill não encontrado[/yellow]") console.print("[dim]Usando template padrão...[/dim]") return DEFAULT_MEETING_PROMPT # PASSO 1: Perguntar se quer auto-gerar console.print("Posso analisar o transcript e sugerir um formato de resumo/ata?") generate = Prompt.ask( "\n💡 Gerar prompt automaticamente?", choices=["s", "n"], default="s" ) if generate == "n": console.print("[dim]✅ Ok, gerando apenas transcript.md (sem ata)[/dim]") return None # Sinaliza: não processar com LLM # PASSO 2: Analisar transcript e SUGERIR tipo console.print("\n[cyan]🔍 Analisando transcript...[/cyan]") suggestion_meta_prompt = f""" Analise este transcript ({len(transcript)} caracteres) e sugira: 1. Tipo de conteúdo (reunião, palestra, entrevista, etc.) 2. Formato de saída recomendado (ata formal, resumo executivo, notas estruturadas) 3. Framework ideal (RISEN, RODES, STAR, etc.) Primeiras 1000 palavras do transcript: {transcript[:4000]} Responda em 2-3 linhas concisas. """ suggested_type = invoke_prompt_engineer(suggestion_meta_prompt) # PASSO 3: Mostrar sugestão e CONFIRMAR console.print("\n[green]💡 Sugestão de formato:[/green]") console.print(Panel(suggested_type, title="Análise do transcript", border_style="green")) confirm_type = Prompt.ask( "\n💡 Usar este formato?", choices=["s", "n"], default="s" ) if confirm_type == "n": console.print("[dim]Usando template padrão...[/dim]") return DEFAULT_MEETING_PROMPT # PASSO 4: Gerar prompt completo baseado na sugestão console.print("\n[cyan]✨ Gerando prompt estruturado...[/cyan]") final_meta_prompt = f""" Crie um prompt completo e estruturado (usando framework apropriado) para: {suggested_type} O prompt deve instruir uma IA a transformar o transcript em um documento profissional e bem formatado em Markdown. """ generated_prompt = invoke_prompt_engineer(final_meta_prompt) # PASSO 5: Mostrar prompt gerado e CONFIRMAR console.print("\n[green]✅ Prompt gerado:[/green]") console.print(Panel(generated_prompt[:600] + ("..." if len(generated_prompt) > 600 else ""), title="Preview", border_style="green")) confirm_final = Prompt.ask( "\n💡 Usar este prompt?", choices=["s", "n"], default="s" ) if confirm_final == "s": return generated_prompt else: console.print("[dim]Usando template padrão...[/dim]") return DEFAULT_MEETING_PROMPT def process_with_llm(transcript, prompt, cli_tool='claude', timeout=300): """ Processa transcript com LLM usando prompt fornecido. Args: transcript: Texto transcrito prompt: Prompt instruindo como processar cli_tool: 'claude' ou 'gh-copilot' timeout: Timeout em segundos Returns: str: Ata/resumo processado """ full_prompt = f"{prompt}\n\n---\n\nTranscrição:\n\n{transcript}" try: with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), transient=True ) as progress: progress.add_task(description=f"🤖 Processando com {cli_tool}...", total=None) if cli_tool == 'claude': result = subprocess.run( ['claude', '-'], input=full_prompt, capture_output=True, text=True, timeout=timeout ) elif cli_tool == 'gh-copilot': result = subprocess.run( ['gh', 'copilot', 'suggest', '-t', 'shell', full_prompt], capture_output=True, text=True, timeout=timeout ) else: raise ValueError(f"CLI tool desconhecido: {cli_tool}") if result.returncode == 0: return result.stdout.strip() else: console.print(f"[red]❌ Erro ao processar com {cli_tool}[/red]") console.print(f"[dim]{result.stderr[:200]}[/dim]") return None except subprocess.TimeoutExpired: console.print(f"[red]❌ Timeout após {timeout}s[/red]") return None except Exception as e: console.print(f"[red]❌ Erro: {e}[/red]") return None def transcribe_audio(audio_file, model="base"): """ Transcreve áudio usando Whisper com barra de progresso. Returns: dict: {language, duration, segments: [{start, end, text}]} """ console.print(f"\n[cyan]🎙️ Transcrevendo áudio com {TRANSCRIBER}...[/cyan]") try: if TRANSCRIBER == "faster-whisper": model_obj = WhisperModel(model, device="cpu", compute_type="int8") segments, info = model_obj.transcribe( audio_file, language=None, vad_filter=True, word_timestamps=True ) data = { "language": info.language, "language_probability": round(info.language_probability, 2), "duration": info.duration, "segments": [] } # Converter generator em lista com progresso console.print("[dim]Processando segmentos...[/dim]") for segment in tqdm(segments, desc="Segmentos", unit="seg"): data["segments"].append({ "start": round(segment.start, 2), "end": round(segment.end, 2), "text": segment.text.strip() }) else: # whisper original import whisper model_obj = whisper.load_model(model) result = model_obj.transcribe(audio_file, word_timestamps=True) data = { "language": result["language"], "duration": result["segments"][-1]["end"] if result["segments"] else 0, "segments": result["segments"] } console.print(f"[green]✅ Transcrição completa! Idioma: {data['language'].upper()}[/green]") console.print(f"[dim] {len(data['segments'])} segmentos processados[/dim]") return data except Exception as e: console.print(f"[red]❌ Erro na transcrição: {e}[/red]") sys.exit(1) def save_outputs(transcript_text, ata_text, audio_file, output_dir="."): """ Salva transcript e ata em arquivos .md com timestamp. Returns: tuple: (transcript_path, ata_path or None) """ timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") base_name = Path(audio_file).stem # Sempre salva transcript transcript_filename = f"transcript-{timestamp}.md" transcript_path = Path(output_dir) / transcript_filename with open(transcript_path, 'w', encoding='utf-8') as f: f.write(transcript_text) console.print(f"[green]✅ Transcript salvo:[/green] {transcript_filename}") # Salva ata se existir ata_path = None if ata_text: ata_filename = f"ata-{timestamp}.md" ata_path = Path(output_dir) / ata_filename with open(ata_path, 'w', encoding='utf-8') as f: f.write(ata_text) console.print(f"[green]✅ Ata salva:[/green] {ata_filename}") return str(transcript_path), str(ata_path) if ata_path else None def main(): """Função principal.""" import argparse parser = argparse.ArgumentParser(description="Audio Transcriber v1.1.0") parser.add_argument("audio_file", help="Arquivo de áudio para transcrever") parser.add_argument("--prompt", help="Prompt customizado para processar transcript") parser.add_argument("--model", default="base", help="Modelo Whisper (tiny/base/small/medium/large)") parser.add_argument("--output-dir", default=".", help="Diretório de saída") args = parser.parse_args() # Verificar arquivo existe if not os.path.exists(args.audio_file): console.print(f"[red]❌ Arquivo não encontrado: {args.audio_file}[/red]") sys.exit(1) console.print("[bold cyan]🎵 Audio Transcriber v1.1.0[/bold cyan]\n") # Step 1: Transcrever transcription_data = transcribe_audio(args.audio_file, model=args.model) # Gerar texto do transcript transcript_text = f"# Transcrição de Áudio\n\n" transcript_text += f"**Arquivo:** {Path(args.audio_file).name}\n" transcript_text += f"**Idioma:** {transcription_data['language'].upper()}\n" transcript_text += f"**Data:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n" transcript_text += "---\n\n## Transcrição Completa\n\n" for seg in transcription_data["segments"]: start_min = int(seg["start"] // 60) start_sec = int(seg["start"] % 60) end_min = int(seg["end"] // 60) end_sec = int(seg["end"] % 60) transcript_text += f"**[{start_min:02d}:{start_sec:02d} → {end_min:02d}:{end_sec:02d}]** \n{seg['text']}\n\n" # Step 2: Detectar CLI cli_tool = detect_cli_tool() if not cli_tool: console.print("\n[yellow]⚠️ Nenhuma CLI de IA detectada (Claude ou GitHub Copilot)[/yellow]") console.print("[dim]ℹ️ Salvando apenas transcript.md...[/dim]") save_outputs(transcript_text, None, args.audio_file, args.output_dir) console.print("\n[cyan]💡 Para gerar ata/resumo:[/cyan]") console.print(" - Instale Claude CLI: pip install claude-cli") console.print(" - Ou GitHub Copilot CLI já está instalado (gh copilot)") return console.print(f"\n[green]✅ CLI detectada: {cli_tool}[/green]") # Step 3: Workflow de prompt final_prompt = handle_prompt_workflow(args.prompt, transcript_text) if final_prompt is None: # Usuário recusou processamento save_outputs(transcript_text, None, args.audio_file, args.output_dir) return # Step 4: Processar com LLM ata_text = process_with_llm(transcript_text, final_prompt, cli_tool) if ata_text: console.print("[green]✅ Ata gerada com sucesso![/green]") else: console.print("[yellow]⚠️ Falha ao gerar ata, salvando apenas transcript[/yellow]") # Step 5: Salvar arquivos console.print("\n[cyan]💾 Salvando arquivos...[/cyan]") save_outputs(transcript_text, ata_text, args.audio_file, args.output_dir) console.print("\n[bold green]✅ Concluído![/bold green]") if __name__ == "__main__": main()