from __future__ import annotations import json import logging import time import os from pathlib import Path from typing import Dict, List import requests from video_render.config import BASE_DIR, Settings from video_render.transcription import TranscriptionResult logger = logging.getLogger(__name__) OPENROUTER_ENDPOINT = os.environ.get("OPENROUTER_API_URL", "https://openrouter.ai/api/v1/chat/completions") class OpenRouterCopywriter: def __init__(self, settings: Settings) -> None: if not settings.openrouter.api_key: raise RuntimeError("OPENROUTER_API_KEY nao foi definido") self.settings = settings prompt_path = Path(settings.openrouter.prompt_path) if not prompt_path.is_absolute(): prompt_path = BASE_DIR / prompt_path if not prompt_path.exists(): raise FileNotFoundError(f"Prompt nao encontrado: {prompt_path}") self.highlights_prompt_template = prompt_path.read_text(encoding="utf-8") def generate_highlights(self, transcription: TranscriptionResult) -> List[Dict]: """Generate video highlights using OpenRouter GPT-OSS with retry logic.""" payload = { "transcript": transcription.full_text, "segments": [ { "start": segment.start, "end": segment.end, "text": segment.text, } for segment in transcription.segments ], } body = { "model": self.settings.openrouter.model, "temperature": self.settings.openrouter.temperature, "messages": [ {"role": "system", "content": self.highlights_prompt_template}, { "role": "user", "content": json.dumps(payload, ensure_ascii=False), }, ], } headers = { "Authorization": f"Bearer {self.settings.openrouter.api_key}", "Content-Type": "application/json", "X-Title": "Video Render - Highlights Detection" } logger.info(f"Calling OpenRouter with model: {self.settings.openrouter.model}") logger.debug(f"Request payload keys: transcript_length={len(payload['transcript'])}, segments_count={len(payload['segments'])}") # Retry configuration for rate limits (especially free tier) max_retries = 5 base_delay = 5 # Start with 5s delay for attempt in range(max_retries): try: response = requests.post( url=OPENROUTER_ENDPOINT, data=json.dumps(body), headers=headers, timeout=120, ) response.raise_for_status() data = response.json() break except requests.exceptions.HTTPError as exc: if exc.response.status_code == 429: if attempt < max_retries - 1: # Exponential backoff: 5s, 10s, 20s, 40s, 80s delay = base_delay * (2 ** attempt) logger.warning(f"Rate limit atingido (429). Aguardando {delay}s antes de tentar novamente (tentativa {attempt + 1}/{max_retries})") time.sleep(delay) continue else: logger.error("Rate limit atingido apos todas as tentativas") logger.error("Solucao: Use um modelo pago ou adicione creditos na OpenRouter") raise RuntimeError("OpenRouter rate limit excedido") from exc else: logger.error(f"OpenRouter API request falhou com status {exc.response.status_code}: {exc}") raise RuntimeError("OpenRouter API request falhou") from exc except Exception as exc: logger.error("OpenRouter API request falhou: %s", exc) raise RuntimeError("OpenRouter API request falhou") from exc # Debug: log response structure logger.info(f"OpenRouter response keys: {list(data.keys())}") if "error" in data: logger.error(f"OpenRouter API error: {data.get('error')}") raise RuntimeError(f"OpenRouter API error: {data.get('error')}") choices = data.get("choices") or [] if not choices: logger.error(f"OpenRouter response completa: {json.dumps(data, indent=2)}") raise RuntimeError("OpenRouter nao retornou escolhas") message = choices[0].get("message", {}).get("content") if not message: raise RuntimeError("Resposta do OpenRouter sem conteudo") parsed = self._extract_json(message) highlights = parsed.get("highlights") if not isinstance(highlights, list): raise ValueError("Resposta do OpenRouter invalida: campo 'highlights' ausente") valid_highlights = [] for highlight in highlights: try: start = float(highlight.get("start", 0)) end = float(highlight.get("end", 0)) summary = str(highlight.get("summary", "")).strip() if start < 0 or end < 0: logger.warning(f"Highlight ignorado: timestamps negativos (start={start}, end={end})") continue if end <= start: logger.warning(f"Highlight ignorado: end <= start (start={start}, end={end})") continue duration = end - start if duration < 60: logger.warning(f"Highlight ignorado: muito curto ({duration}s, minimo 45s)") continue if duration > 120: logger.warning(f"Highlight ignorado: muito longo ({duration}s, maximo 90s)") continue if not summary: logger.warning(f"Highlight ignorado: summary vazio") continue valid_highlights.append({ "start": start, "end": end, "summary": summary }) except (TypeError, ValueError) as e: logger.warning(f"Highlight invalido ignorado: {highlight} - {e}") continue if not valid_highlights: logger.warning("Nenhum highlight valido retornado pelo OpenRouter") total_duration = 75.0 if transcription.segments: total_duration = max(seg.end for seg in transcription.segments) fallback_end = min(75.0, total_duration) if fallback_end < 60.0: fallback_end = min(60.0, total_duration) return [{ "start": 0.0, "end": fallback_end, "summary": "Trecho inicial do video (fallback automatico)" }] logger.info(f"OpenRouter retornou {len(valid_highlights)} highlights validos") return valid_highlights def generate_titles(self, highlights: List[Dict]) -> List[str]: if not highlights: return [] prompt = ( "Voce e um copywriter especializado em titulos curtos e virais para reels.\n" "Recebera uma lista de trechos destacados de um video com resumo e tempo.\n" "Produza um titulo envolvente (ate 60 caracteres) para cada item.\n" "Responda apenas em JSON com a seguinte estrutura:\n" '{"titles": ["titulo 1", "titulo 2"]}\n' "Titulos devem ser em portugues, usar verbos fortes e refletir o resumo." ) user_payload = { "highlights": [ { "start": item.get("start"), "end": item.get("end"), "summary": item.get("summary"), } for item in highlights ] } body = { "model": self.settings.openrouter.model, "temperature": self.settings.openrouter.temperature, "messages": [ {"role": "system", "content": prompt}, { "role": "user", "content": json.dumps(user_payload, ensure_ascii=False), }, ], } headers = { "Authorization": f"Bearer {self.settings.openrouter.api_key}", "Content-Type": "application/json", } response = requests.post( url=OPENROUTER_ENDPOINT, data=json.dumps(body), headers=headers, timeout=120, ) response.raise_for_status() data = response.json() choices = data.get("choices") or [] if not choices: raise RuntimeError("OpenRouter nao retornou escolhas") message = choices[0].get("message", {}).get("content") if not message: raise RuntimeError("Resposta do OpenRouter sem conteudo") parsed = self._extract_json(message) titles = parsed.get("titles") if not isinstance(titles, list): raise ValueError("Resposta do OpenRouter invalida: campo 'titles'") return [str(title) for title in titles] @staticmethod def _extract_json(response_text: str) -> Dict: try: return json.loads(response_text) except json.JSONDecodeError: start = response_text.find("{") end = response_text.rfind("}") if start == -1 or end == -1: raise subset = response_text[start : end + 1] return json.loads(subset)