216 lines
7.1 KiB
Python
216 lines
7.1 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from google import genai
|
|
from google.genai import types as genai_types
|
|
import requests
|
|
|
|
from video_render.config import BASE_DIR, Settings
|
|
from video_render.transcription import TranscriptionResult
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
OPENROUTER_ENDPOINT = "https://openrouter.ai/api/v1/chat/completions"
|
|
|
|
|
|
class GeminiHighlighter:
|
|
def __init__(self, settings: Settings) -> None:
|
|
if not settings.gemini.api_key:
|
|
raise RuntimeError("GEMINI_API_KEY nao foi definido")
|
|
|
|
prompt_path = Path(settings.gemini.prompt_path)
|
|
|
|
if not prompt_path.is_absolute():
|
|
prompt_path = BASE_DIR / prompt_path
|
|
|
|
if not prompt_path.exists():
|
|
raise FileNotFoundError(f"Prompt do Gemini nao encontrado: {prompt_path}")
|
|
|
|
self.prompt_template = prompt_path.read_text(encoding="utf-8")
|
|
self.settings = settings
|
|
self.client = genai.Client()
|
|
|
|
def generate_highlights(self, transcription: TranscriptionResult) -> List[Dict]:
|
|
payload = {
|
|
"transcript": transcription.full_text,
|
|
"segments": [
|
|
{
|
|
"start": segment.start,
|
|
"end": segment.end,
|
|
"text": segment.text,
|
|
}
|
|
for segment in transcription.segments
|
|
],
|
|
}
|
|
|
|
try:
|
|
response = self._call_gemini(payload)
|
|
except Exception as exc:
|
|
logger.error("Gemini API request falhou: %s", exc)
|
|
raise RuntimeError("Gemini API request falhou") from exc
|
|
|
|
raw_text = self._extract_response_text(response)
|
|
|
|
parsed = self._extract_json(raw_text)
|
|
highlights = parsed.get("highlights")
|
|
if not isinstance(highlights, list):
|
|
raise ValueError("Resposta do Gemini invalida: campo 'highlights' ausente")
|
|
return highlights
|
|
|
|
def _call_gemini(self, payload: Dict[str, Any]) -> Any:
|
|
contents = [
|
|
{
|
|
"role": "user",
|
|
"parts": [
|
|
{"text": self.prompt_template},
|
|
{"text": json.dumps(payload, ensure_ascii=False)},
|
|
],
|
|
}
|
|
]
|
|
|
|
request_kwargs: Dict[str, Any] = {
|
|
"model": self.settings.gemini.model,
|
|
"contents": contents,
|
|
}
|
|
|
|
config = self._build_generation_config()
|
|
if config is not None:
|
|
request_kwargs["config"] = config
|
|
|
|
return self.client.models.generate_content(**request_kwargs)
|
|
|
|
def _build_generation_config(self) -> Optional[genai_types.GenerateContentConfig]:
|
|
config_kwargs: Dict[str, Any] = {}
|
|
if self.settings.gemini.temperature is not None:
|
|
config_kwargs["temperature"] = self.settings.gemini.temperature
|
|
if self.settings.gemini.top_p is not None:
|
|
config_kwargs["top_p"] = self.settings.gemini.top_p
|
|
if self.settings.gemini.top_k is not None:
|
|
config_kwargs["top_k"] = self.settings.gemini.top_k
|
|
|
|
if not config_kwargs:
|
|
return None
|
|
|
|
return genai_types.GenerateContentConfig(**config_kwargs)
|
|
|
|
@staticmethod
|
|
def _extract_response_text(response: Any) -> str:
|
|
text = getattr(response, "text", None)
|
|
if text:
|
|
return str(text).strip()
|
|
|
|
candidates = getattr(response, "candidates", None) or []
|
|
for candidate in candidates:
|
|
content = getattr(candidate, "content", None)
|
|
if not content:
|
|
continue
|
|
parts = getattr(content, "parts", None) or []
|
|
for part in parts:
|
|
part_text = getattr(part, "text", None)
|
|
if part_text:
|
|
return str(part_text).strip()
|
|
|
|
raise RuntimeError("Resposta do Gemini sem texto")
|
|
|
|
@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)
|
|
|
|
|
|
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
|
|
|
|
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)
|