Spaces:
Running
Running
import os | |
import logging | |
import json | |
import requests | |
from fastapi import APIRouter, HTTPException | |
from pydantic import BaseModel | |
from google import genai | |
from google.genai import types | |
from datetime import datetime | |
from zoneinfo import ZoneInfo | |
import locale | |
import re | |
# Configurar logging | |
logger = logging.getLogger(__name__) | |
router = APIRouter() | |
class NewsRequest(BaseModel): | |
content: str | |
sources_url: str # URL do arquivo fontes.txt | |
class NewsResponse(BaseModel): | |
title: str | |
subhead: str | |
content: str | |
sources: list[str] # Lista de URLs/links utilizados | |
def get_brazilian_date_string(): | |
""" | |
Retorna a data atual formatada em português brasileiro. | |
Implementa fallbacks robustos para diferentes sistemas operacionais. | |
""" | |
try: | |
# Tenta configurar o locale brasileiro | |
locale_variants = [ | |
'pt_BR.UTF-8', | |
'pt_BR.utf8', | |
'pt_BR', | |
'Portuguese_Brazil.1252', | |
'Portuguese_Brazil', | |
'pt_BR.ISO8859-1', | |
] | |
locale_set = False | |
for loc in locale_variants: | |
try: | |
locale.setlocale(locale.LC_TIME, loc) | |
locale_set = True | |
break | |
except locale.Error: | |
continue | |
if not locale_set: | |
locale.setlocale(locale.LC_TIME, '') | |
now = datetime.now(ZoneInfo("America/Sao_Paulo")) | |
# Dicionários para tradução manual (fallback) | |
meses = { | |
1: 'janeiro', 2: 'fevereiro', 3: 'março', 4: 'abril', | |
5: 'maio', 6: 'junho', 7: 'julho', 8: 'agosto', | |
9: 'setembro', 10: 'outubro', 11: 'novembro', 12: 'dezembro' | |
} | |
dias_semana = { | |
0: 'segunda-feira', 1: 'terça-feira', 2: 'quarta-feira', | |
3: 'quinta-feira', 4: 'sexta-feira', 5: 'sábado', 6: 'domingo' | |
} | |
try: | |
if locale_set: | |
try: | |
date_string = now.strftime("%-d de %B de %Y (%A)") | |
except ValueError: | |
try: | |
date_string = now.strftime("%#d de %B de %Y (%A)") | |
except ValueError: | |
date_string = now.strftime("%d de %B de %Y (%A)") | |
if date_string.startswith('0'): | |
date_string = date_string[1:] | |
date_string = date_string.replace(date_string.split('(')[1].split(')')[0], | |
date_string.split('(')[1].split(')')[0].lower()) | |
else: | |
dia = now.day | |
mes = meses[now.month] | |
ano = now.year | |
dia_semana = dias_semana[now.weekday()] | |
date_string = f"{dia} de {mes} de {ano} ({dia_semana})" | |
except Exception: | |
dia = now.day | |
mes = meses[now.month] | |
ano = now.year | |
dia_semana = dias_semana[now.weekday()] | |
date_string = f"{dia} de {mes} de {ano} ({dia_semana})" | |
return date_string | |
except Exception: | |
now = datetime.now(ZoneInfo("America/Sao_Paulo")) | |
date_string = now.strftime("%d de %B de %Y") | |
return date_string | |
def download_sources_file(url: str) -> str: | |
""" | |
Baixa o arquivo fontes.txt da URL fornecida. | |
""" | |
try: | |
response = requests.get(url, timeout=30) | |
response.raise_for_status() | |
return response.text | |
except Exception as e: | |
logger.error(f"Erro ao baixar arquivo de fontes: {e}") | |
raise HTTPException(status_code=400, detail=f"Erro ao baixar arquivo de fontes: {str(e)}") | |
def extract_text_from_response(response): | |
""" | |
Extrai o texto da resposta de forma robusta. | |
""" | |
response_text = "" | |
if hasattr(response, 'text') and response.text: | |
return response.text | |
if hasattr(response, 'candidates') and response.candidates: | |
for candidate in response.candidates: | |
if not hasattr(candidate, 'content') or not candidate.content: | |
continue | |
content = candidate.content | |
if not hasattr(content, 'parts') or content.parts is None: | |
continue | |
try: | |
parts_list = list(content.parts) if content.parts else [] | |
for part in parts_list: | |
if hasattr(part, 'text') and part.text: | |
response_text += part.text | |
except Exception: | |
continue | |
return response_text | |
def extract_sources_from_response(response): | |
""" | |
Extrai as fontes (URLs) do grounding metadata. | |
""" | |
sources = [] | |
if not (hasattr(response, 'candidates') and response.candidates): | |
return sources | |
for candidate in response.candidates: | |
if not (hasattr(candidate, 'grounding_metadata') and candidate.grounding_metadata): | |
continue | |
grounding_metadata = candidate.grounding_metadata | |
if hasattr(grounding_metadata, 'grounding_chunks') and grounding_metadata.grounding_chunks: | |
for chunk in grounding_metadata.grounding_chunks: | |
try: | |
if (hasattr(chunk, 'web') and chunk.web and | |
hasattr(chunk.web, 'uri') and chunk.web.uri): | |
uri = chunk.web.uri | |
if uri and uri not in sources: | |
sources.append(uri) | |
except Exception: | |
continue | |
return sources | |
async def rewrite_news(news: NewsRequest): | |
""" | |
Endpoint para reescrever notícias usando o modelo Gemini com arquivo de fontes. | |
""" | |
try: | |
# Verificar API key | |
api_key = os.environ.get("GEMINI_API_KEY") | |
if not api_key: | |
raise HTTPException(status_code=500, detail="API key não configurada") | |
# Baixar arquivo de fontes | |
sources_content = download_sources_file(news.sources_url) | |
client = genai.Client(api_key=api_key) | |
model = "gemini-2.5-pro" | |
# Obter data formatada | |
date_string = get_brazilian_date_string() | |
# Instruções do sistema | |
SYSTEM_INSTRUCTIONS = f""" | |
Você é um jornalista brasileiro, escrevendo para portais digitais. Sua missão é transformar notícias internacionais em matérias originais, atualizadas e envolventes para o público brasileiro. | |
ESTILO DE ESCRITA: | |
- Tom natural, direto e conversacional, sem frases feitas ou jargões jornalísticos. | |
- Varie estruturas e conectivos para evitar robotização. Ou seja, sem frases redundantes, exemplo: "destacando como a experiência pode ser um divisor de águas profissional", "reafirma a força criativa do país no cenário global", "A revelação contextualizou não apenas sua performance na dança, mas também". É para noticiar a notícia de fato, sempre buscando mais informações que se conectam, e não opiniões pessoais. | |
- Sem linguagem militante ou juízos morais: mantenha tom informativo e predominantemente conservador / aspectro de Direita que o jornal tem. | |
- Evite opiniões genéricas; foque em fatos e contexto | |
CONTEÚDO, ESTRUTURA E ABORDAGEM: | |
- Use a notícia-base como ponto de partida, não como roteiro. Avalie o fontes.txt por completo e extraia absolutamente todas as coisas relevantes para formentar a notícia. Não traga informações falsas ou inventadas. Na dúvida, não insira. | |
- Sempre que possível, inclua títulos de obras em Português do Brasil. | |
- Termine com uma informação concreta (data, lançamento, próximos passos), nunca com opinião genérica. Ou seja, a conclusão da noticia deve ser com mais noticia, sem redundância genérica e robótica. | |
- Otimize para leitura digital e SEO (parágrafos bem segmentados, palavras chaves, etc) | |
FORMATO: | |
<headline>título aqui</headline> | |
<subhead>subtítulo aqui</subhead> | |
<body>conteúdo aqui</body> | |
Use <strong> para destaques e <em> para títulos de obras ou citações. | |
TÍTULOS: | |
- Padrão brasileiro: só a primeira palavra em maiúscula (exceto nomes próprios e títulos de filmes/séries/obras, etc.) | |
- Seja claro, direto e específico... Se houver, não inclua spoilers no título, apenas no body. | |
O resultado deve soar como uma matéria escrita por um profissional experiente, não por IA. Seja preciso, atual e interessante. Sempre complete a notícia com acontecimentos que se ligam, sempre contextualize tudo para o leitor. A data de hoje é {date_string} | |
""" | |
# Exemplos (mantidos os mesmos do código original) | |
EXAMPLE_INPUT_1 = """ | |
News base: Ed Helms revealed in an interview that he was nervous about his parents' reaction to the film The Hangover, but in the end everything worked out and her mother loved the movie. The article is out of date, more information is needed. | |
""" | |
EXAMPLE_OUTPUT_1 = """<headline>"Se Beber, Não Case!": Ed Helms, o Dr. Stuart, revela medo do que os pais iriam pensar, mas tudo deu certo</headline> | |
<subhead>Em uma carreira repleta de surpresas e sucesso internacional, o ator relembra o nervosismo que antecedeu a estreia da comédia que o tornou famoso.</subhead> | |
<body> | |
<p><strong>Ed Helms</strong> nunca escondeu o fato de que sua participação em <strong>Se Beber, Não Case!</strong> foi um choque cultural, especialmente para seus pais. Em uma entrevista recente ao podcast de <strong>Ted Danson</strong>, <em>Where Everybody Knows Your Name</em>, o ator falou sobre a ansiedade que sentiu ao imaginar a reação da família à comédia para maiores que o transformou em astro de cinema.</p> | |
<p>Helms, que foi criado em um lar sulista com valores socialmente conservadores, revelou que, embora o ambiente fosse politicamente progressista, algumas situações, como dentes arrancados, casamentos embriagados e até tigres no banheiro, eram muito diferentes do que seus pais consideravam apropriado. O ator brincou: <em>"Não foi pra isso que me criaram"</em>, fazendo alusão ao enredo caótico do filme de 2009. Ele acrescentou que, embora seus pais já tivessem assistido a algumas de suas performances em programas como <em>The Daily Show</em> e <em>The Office</em>, o que ajudou a criar certa tolerância, o filme ainda o deixava nervoso.</p> | |
<p>Estrelando sua primeira grande produção, Helms levou os pais para a estreia quando tinha 35 anos. No entanto, foi surpreendido ao ver sua mãe chorando quando as luzes se acenderam. <em>"Pensei: 'Pronto. Acabei de partir o coração da minha mãe'"</em>, recordou. O momento de tensão, porém, durou pouco: ela o tranquilizou dizendo que o filme havia sido hilário.</p> | |
<p><strong>Se Beber, Não Case!</strong>, dirigido por <strong>Todd Phillips</strong>, foi um sucesso comercial, arrecadando aproximadamente <strong>469 milhões de dólares</strong> em todo o mundo e se tornando a comédia para maiores de classificação indicativa de maior bilheteria até então. A popularidade do filme resultou em duas sequências, lançadas em 2011 e 2013, e consolidou o "bando de lobos" formado por <strong>Helms</strong>, <strong>Bradley Cooper</strong> e <strong>Zach Galifianakis</strong> como um dos times cômicos mais icônicos do cinema moderno.</p> | |
<p>Sobre a possibilidade de um quarto filme, <strong>Bradley Cooper</strong> afirmou em 2023 que toparia participar sem hesitar, principalmente pela chance de reencontrar colegas e diretor. Ainda assim, reconheceu que o projeto é improvável, já que <strong>Phillips</strong> está atualmente focado em empreendimentos de maior escala, como a série de filmes <em>Coringa</em>.</p> | |
</body>""" | |
# Configuração da ferramenta de pesquisa | |
grounding_tool = types.Tool( | |
google_search=types.GoogleSearch() | |
) | |
config = types.GenerateContentConfig( | |
system_instruction=SYSTEM_INSTRUCTIONS, | |
thinking_config=types.ThinkingConfig( | |
thinking_budget=-1, | |
), | |
tools=[grounding_tool], | |
response_mime_type="text/plain", | |
max_output_tokens=4096, | |
temperature=0.8, | |
) | |
# Conteúdo da conversa | |
contents = [ | |
# Exemplo | |
types.Content( | |
role="user", | |
parts=[ | |
types.Part.from_text(text=EXAMPLE_INPUT_1) | |
] | |
), | |
types.Content( | |
role="model", | |
parts=[ | |
types.Part.from_text(text=EXAMPLE_OUTPUT_1) | |
] | |
), | |
# Notícia atual com arquivo de fontes | |
types.Content( | |
role="user", | |
parts=[ | |
types.Part.from_text(text=f"News base: {news.content}. The article is out of date, more information is needed."), | |
types.Part.from_text(text=f"Fontes adicionais disponíveis:\n\n{sources_content}") | |
] | |
) | |
] | |
# Gerar conteúdo | |
response = client.models.generate_content( | |
model=model, | |
contents=contents, | |
config=config | |
) | |
# Extrair texto e fontes | |
response_text = extract_text_from_response(response) | |
sources = extract_sources_from_response(response) | |
# Verificar se o texto está vazio | |
if not response_text or response_text.strip() == "": | |
raise HTTPException( | |
status_code=500, | |
detail="Modelo não retornou conteúdo válido" | |
) | |
# Extração do título, subtítulo e conteúdo | |
title_match = re.search(r"<headline>(.*?)</headline>", response_text, re.DOTALL) | |
title = title_match.group(1).strip() if title_match else "Título não encontrado" | |
subhead_match = re.search(r"<subhead>(.*?)</subhead>", response_text, re.DOTALL) | |
subhead = subhead_match.group(1).strip() if subhead_match else "Subtítulo não encontrado" | |
body_match = re.search(r"<body>(.*?)</body>", response_text, re.DOTALL) | |
if body_match: | |
content = body_match.group(1).strip() | |
else: | |
body_start_match = re.search(r"<body>(.*)", response_text, re.DOTALL) | |
if body_start_match: | |
content = body_start_match.group(1).strip() | |
else: | |
content = "Conteúdo não encontrado" | |
return NewsResponse(title=title, subhead=subhead, content=content, sources=sources) | |
except HTTPException: | |
raise | |
except Exception as e: | |
logger.error(f"Erro na reescrita: {str(e)}") | |
raise HTTPException(status_code=500, detail=str(e)) |