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 | |
from pathlib import Path | |
# Configurar logging | |
logger = logging.getLogger(__name__) | |
router = APIRouter() | |
class NewsRequest(BaseModel): | |
content: str | |
file_id: str # ID do arquivo temporário ao invés da URL | |
class NewsResponse(BaseModel): | |
title: str | |
subhead: str | |
content: str | |
sources: list[str] # Lista de URLs/links utilizados | |
# Referência ao diretório de arquivos temporários (deve ser o mesmo do outro módulo) | |
TEMP_DIR = Path("/tmp") | |
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 load_sources_file(file_id: str) -> str: | |
""" | |
Carrega o arquivo de fontes pelo ID do arquivo temporário. | |
""" | |
try: | |
# Constrói o caminho do arquivo | |
file_path = TEMP_DIR / f"fontes_{file_id}.txt" | |
# Verifica se o arquivo existe | |
if not file_path.exists(): | |
raise HTTPException( | |
status_code=404, | |
detail=f"Arquivo temporário não encontrado ou expirado: {file_id}" | |
) | |
# Lê o conteúdo do arquivo | |
with open(file_path, 'r', encoding='utf-8') as f: | |
file_content = f.read() | |
# Se for um JSON, extrai os dados; caso contrário, retorna o conteúdo direto | |
try: | |
data = json.loads(file_content) | |
# Se contém 'results', formata os dados para o Gemini | |
if 'results' in data and isinstance(data['results'], list): | |
formatted_content = "" | |
for idx, result in enumerate(data['results'], 1): | |
formatted_content += f"\n--- FONTE {idx} ---\n" | |
formatted_content += f"Termo: {result.get('term', 'N/A')}\n" | |
formatted_content += f"URL: {result.get('url', 'N/A')}\n" | |
formatted_content += f"Idade: {result.get('age', 'N/A')}\n" | |
formatted_content += f"Conteúdo:\n{result.get('text', 'N/A')}\n" | |
formatted_content += "-" * 50 + "\n" | |
return formatted_content | |
else: | |
return file_content | |
except json.JSONDecodeError: | |
# Se não for JSON válido, retorna o conteúdo como texto | |
return file_content | |
except FileNotFoundError: | |
raise HTTPException( | |
status_code=404, | |
detail=f"Arquivo temporário não encontrado: {file_id}" | |
) | |
except PermissionError: | |
raise HTTPException( | |
status_code=500, | |
detail=f"Erro de permissão ao acessar arquivo: {file_id}" | |
) | |
except Exception as e: | |
logger.error(f"Erro ao carregar arquivo de fontes {file_id}: {e}") | |
raise HTTPException( | |
status_code=500, | |
detail=f"Erro ao carregar arquivo de fontes: {str(e)}" | |
) | |
def extract_text_from_response(response): | |
""" | |
Extrai o texto da resposta de forma robusta com debug. | |
""" | |
logger.info(f"Tipo da resposta: {type(response)}") | |
# Método 1: Tentar acessar response.text diretamente | |
try: | |
text_content = getattr(response, 'text', None) | |
if text_content: | |
logger.info(f"Texto extraído via response.text: {len(text_content)} caracteres") | |
return text_content | |
else: | |
logger.info("response.text existe mas está vazio/None") | |
except Exception as e: | |
logger.error(f"Erro ao acessar response.text: {e}") | |
# Método 2: Verificar candidates | |
if hasattr(response, 'candidates') and response.candidates: | |
logger.info(f"Encontrados {len(response.candidates)} candidates") | |
for i, candidate in enumerate(response.candidates): | |
logger.info(f"Processando candidate {i}") | |
# Verificar se tem content | |
if hasattr(candidate, 'content') and candidate.content: | |
content = candidate.content | |
logger.info(f"Candidate {i} tem content") | |
# Verificar se tem parts | |
if hasattr(content, 'parts') and content.parts: | |
try: | |
parts_list = list(content.parts) | |
logger.info(f"Content tem {len(parts_list)} parts") | |
response_text = "" | |
for j, part in enumerate(parts_list): | |
logger.info(f"Processando part {j}, tipo: {type(part)}") | |
# Tentar várias formas de acessar o texto | |
part_text = None | |
if hasattr(part, 'text'): | |
part_text = getattr(part, 'text', None) | |
if part_text: | |
logger.info(f"Part {j} tem texto: {len(part_text)} caracteres") | |
response_text += part_text | |
else: | |
logger.info(f"Part {j} não tem texto ou está vazio") | |
if response_text: | |
return response_text | |
except Exception as e: | |
logger.error(f"Erro ao processar parts do candidate {i}: {e}") | |
# Método 3: Tentar usar método _get_text() se existir | |
try: | |
if hasattr(response, '_get_text'): | |
text_content = response._get_text() | |
if text_content: | |
logger.info(f"Texto extraído via _get_text(): {len(text_content)} caracteres") | |
return text_content | |
except Exception as e: | |
logger.error(f"Erro ao usar _get_text(): {e}") | |
# Método 4: Debug - tentar inspecionar a estrutura real | |
try: | |
logger.info("Tentando debug da estrutura:") | |
if hasattr(response, 'candidates') and response.candidates: | |
candidate = response.candidates[0] | |
logger.info(f"Primeiro candidate: {type(candidate)}") | |
logger.info(f"Atributos do candidate: {dir(candidate)}") | |
if hasattr(candidate, 'content'): | |
content = candidate.content | |
logger.info(f"Content: {type(content)}") | |
logger.info(f"Atributos do content: {dir(content)}") | |
if hasattr(content, 'parts'): | |
logger.info(f"Parts: {type(content.parts)}") | |
try: | |
parts_list = list(content.parts) | |
if parts_list: | |
first_part = parts_list[0] | |
logger.info(f"Primeiro part: {type(first_part)}") | |
logger.info(f"Atributos do part: {dir(first_part)}") | |
except Exception as e: | |
logger.error(f"Erro ao inspecionar parts: {e}") | |
except Exception as e: | |
logger.error(f"Erro no debug da estrutura: {e}") | |
return "" | |
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") | |
# Carregar arquivo de fontes pelo ID | |
sources_content = load_sources_file(news.file_id) | |
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, detalhadas e atualizadas para o público brasileiro. Sempre use a notícia-base como ponto de partida, mas consulte o arquivo fontes.txt para extrair todas as informações relevantes, complementando fatos, contexto, dados e antecedentes. Não invente informações; na dúvida, não insira. | |
Seu estilo de escrita deve ser direto, claro e conversacional, sem jargões ou floreios desnecessários. Frases curtas e bem estruturadas, parágrafos segmentados para leitura digital e SEO. Evite repetições, clichês e generalizações. Mantenha tom informativo e predominantemente conservador, sem juízos de valor ou opiniões pessoais. Não use linguagem woke, de vitimização ou que force interpretações ideológicas. | |
Evite frases redundantes ou genéricas como: | |
- "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" | |
O conteúdo deve priorizar clareza, contexto e completude: | |
- Comece com a informação mais relevante e específica. | |
- Contextualize causas, consequências e conexões com outros acontecimentos. | |
- Inclua dados, datas, lançamentos e fontes confiáveis. | |
- Use citações, títulos de obras e nomes próprios quando pertinentes. | |
- Finalize sempre com fatos concretos, nunca com opinião genérica. | |
Formato da matéria: | |
<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, etc. | |
Padrão de títulos: apenas a primeira palavra em maiúscula (exceto nomes próprios ou títulos de obras). | |
O resultado deve soar como uma reportagem profissional, completa, confiável e envolvente, no estilo do New York Times: informativa, neutra, detalhada, com narrativa que aproxima o leitor dos fatos, mantendo linguagem humana e acessível. Sempre busque complementar a notícia com informações adicionais e contextos do arquivo fontes.txt, evitando opiniões pessoais. | |
A data de hoje é {date_string}. | |
""" | |
# Exemplos | |
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> | |
<instagram_title>Ed Helms e o medo dos pais em “Se Beber, Não Case!”</instagram_title> | |
<instagram_description>Ed Helms recorda o nervosismo que sentiu antes da estreia de "Se Beber, Não Case!", filme que o projetou internacionalmente. O ator, criado em uma família conservadora do sul, temia que o humor ousado do filme, com cenas caóticas, casamentos alcoolizados e até tigres no banheiro, pudesse chocar seus pais. Helms levou sua família ao cinema na estreia e ficou surpreso ao ver sua mãe emocionada e rindo das cenas mais absurdas.\n\nO filme fez um grande sucesso, arrecadando aproximadamente 469 milhões de dólares globalmente, e resultou em duas sequências que solidificaram Helms, Bradley Cooper e Zach Galifianakis como um dos grupos cômicos mais emblemáticos do cinema. Ele também menciona que a possibilidade de um quarto filme é remota, considerando a atenção do diretor Todd Phillips voltada para novos projetos. Além de detalhes sobre a vida familiar, Helms compartilhou informações interessantes sobre o set de filmagem e a interação entre os membros do elenco.\n\nLeia mais sobre essa história no link da nossa bio.</instagram_description>""" | |
EXAMPLE_INPUT_2 = """ | |
News base: The Office spinoff series 'The Paper' has set a September premiere date at Peacock. | |
The new mockumentary series from Greg Daniels and Michael Koman will debut Sept. 4 on Peacock, the streamer announced Thursday. The first four episodes of 'The Paper' will premiere on Sept. 4, with two new episodes dropping every Thursday through Sept. 25. | |
'The Paper' follows the documentary crew that immortalized Dunder Mifflin's Scranton branch in 'The Office' as they find a new subject when they discover a historic Midwestern newspaper and the publisher trying to revive it, according to the official logline. | |
'The Office' fan-favorite Oscar Nuñez returns to the franchise in 'The Paper,' joining series regulars Domhnall Gleeson, Sabrina Impacciatore, Chelsea Frei, Melvin Gregg, Gbemisola Ikumelo, Alex Edelman, Ramona Young and Tim Key. | |
Guest stars for the show include Eric Rahill, Tracy Letts, Molly Ephraim, Mo Welch, Allan Havey, Duane Shepard Sr., Nate Jackson and Nancy Lenehan. | |
'The Paper' was created by Daniels, who created 'The Office,' under his banner Deedle-Dee Productions, and Koman, who has written on 'Nathan for You' and 'SNL.' Produced by Universal Television, a division of Universal Studio Group, 'The Paper' is executive produced by Ricky Gervais, Stephen Merchant, Howard Klein, Ben Silverman and Banijay Americas (formerly Reveille). | |
Daniels serves as a director on the show alongside Ken Kwapis, Yana Gorskaya, Paul Lieberstein, Tazbah Chavez, Jason Woliner, Jennifer Celotta, Matt Sohn, Dave Rogers and Jeff Blitz. | |
'The Office' launched in 2005 on NBC and ran for nine seasons leading up to the series finale in 2013. The cast of the beloved sitcom included Steve Carell, Rainn Wilson, John Krasinski, Jenna Fischer, Mindy Kaling and B.J. Novak, among others. The article is out of date, more information is needed. | |
""" | |
EXAMPLE_OUTPUT_2 = """<headline>Nova série do universo 'The Office' ganha título, data de estreia e um rosto familiar</headline> | |
<subhead>Intitulada 'The Paper', produção de Greg Daniels e Michael Koman chega em setembro com Domhnall Gleeson, Sabrina Impacciatore e o retorno de Oscar Nuñez</subhead> | |
<body> | |
<p>A equipe original de documentaristas de <em>"Insane Daily Life at Dunder Mifflin"</em> voltou ao trabalho, desta vez mudando para uma nova história, três anos após o fim de <em>"The Office"</em>. Após uma década de espera, o derivado da amada série de comédia finalmente saiu do papel e será lançado em <strong>4 de setembro de 2025</strong>. O nome do derivado é <em>"The Paper"</em> e estará disponível na plataforma de streaming <strong>Peacock</strong>.</p> | |
<p>A trama agora se desloca da fictícia <strong>Scranton, Pensilvânia</strong>, para o escritório de um jornal histórico, porém problemático, localizado no meio-oeste dos Estados Unidos, focando em um jornal em dificuldades na região. A equipe busca uma nova história após cobrir a vida de <strong>Michael Scott</strong> e <strong>Dwight Schrute</strong>. Agora, a equipe acompanha o <strong>Toledo Truth Teller</strong>, um jornal em <strong>Toledo, Ohio</strong>, e o editor que tenta reviver o jornal com a ajuda de repórteres voluntários.</p> | |
<p>O novo elenco conta com <strong>Domhnall Gleeson</strong>, ator irlandês famoso por <em>"Ex Machina"</em> e <em>"Questão de Tempo"</em>, ao lado da atriz italiana <strong>Sabrina Impacciatore</strong>, que ganhou amplo reconhecimento por seu papel na segunda temporada de <em>"The White Lotus"</em>. Gleeson interpreta o novo editor otimista do jornal, enquanto Impacciatore atua como gerente de redação.</p> | |
<p>Nas entrevistas mais recentes, Gleeson tenta se distanciar das comparações com o gerente da <strong>Dunder Mifflin</strong>. <em>"Acho que se você tentar competir com o que Steve [Carell] ou Ricky Gervais fizeram, seria um enorme erro,"</em> enfatizou o ator, visando construir uma persona totalmente nova. Ele também revelou ter recebido um tipo de conselho de <strong>John Krasinski</strong> e até de <strong>Steve Carell</strong> para aceitar o papel, especialmente porque se tratava de um projeto de <strong>Greg Daniels</strong>.</p> | |
<p>Como <em>"The Paper"</em> está reintroduzindo os personagens originais, os fãs de longa data da série parecem estar encantados, já que também traz <strong>Oscar Nuñez</strong> reprisando seu papel como o contador <strong>Oscar Martinez</strong>. Oscar, que estava iniciando uma carreira política em <em>"The Office"</em>, agora parece ter se mudado para <strong>Toledo</strong>. <em>"Eu disse ao Sr. Greg Daniels que, se Oscar voltasse, ele provavelmente estaria morando em uma cidade mais agitada e cosmopolita. Greg me ouviu e mudou Oscar para Toledo, Ohio, que tem três vezes a população de Scranton. Então, foi bom ser ouvido"</em>, brincou Nuñez durante um evento da <strong>NBCUniversal</strong>.</p> | |
<p><strong>Greg Daniels</strong>, que anteriormente adaptou <em>"The Office"</em> para o público americano, está em parceria com <strong>Michael Koman</strong>, cocriador de <em>"Nathan for You"</em>, para este novo projeto. Koman e Daniels, junto com <strong>Ricky Gervais</strong> e <strong>Stephen Merchant</strong>, criadores da série britânica original, formam a equipe de produção executiva.</p> | |
<p>A primeira temporada de <em>"The Paper"</em> será dividida em <strong>dez episódios</strong>. Nos Estados Unidos, os <strong>quatro primeiros episódios</strong> estarão disponíveis para streaming em <strong>4 de setembro</strong>. Depois disso, os episódios restantes serão lançados no formato de <strong>dois episódios por semana</strong>, com um total de seis episódios liberados até o final em <strong>25 de setembro</strong>.</p> | |
<p>A série ainda não tem data de estreia confirmada no Brasil, mas a expectativa é de que seja lançada no <strong>Universal+</strong>, serviço de streaming que costuma exibir produções do catálogo da <strong>Peacock</strong>.</p> | |
</body> | |
<instagram_title>“The Paper”, a nova série do universo "The Office", traz novo enredo e velho conhecido</instagram_title> | |
<instagram_description>O mundo de "The Office" retorna com "The Paper", um derivado criado por Greg Daniels e Michael Koman, que será lançado na plataforma Peacock no dia 4 de setembro de 2025. A nova história segue a rotina de um jornal tradicional, mas em crise, na cidade de Toledo, Ohio, com Domhnall Gleeson interpretando um editor otimista e Sabrina Impacciatore como gerente de redação.\n\nPara a felicidade dos fãs, Oscar Nuñez reprisa o papel de Oscar Martinez, agora morando em uma cidade com população três vezes maior que Scranton. O ator fez uma piada de que o criador Greg Daniels atendeu ao seu pedido por um cenário "mais agitado".\n\nA primeira temporada contará com dez episódios, sendo quatro disponibilizados no lançamento inicial e os demais sendo lançados semanalmente até o dia 25 de setembro. Embora ainda não haja uma data confirmada no Brasil, espera-se que a série seja lançada no Universal+.\n\nLeia mais sobre a produção e seus bastidores no link da nossa bio.</instagram_description> | |
""" | |
EXAMPLE_INPUT_3 = """ | |
News base: Noah Centineo Attached to Play Rambo in Prequel Movie 'John Rambo' | |
""" | |
EXAMPLE_OUTPUT_3 = """<headline>Noah Centineo é o novo Rambo em filme que contará a origem do personagem</headline> | |
<subhead>Ator de 'Para Todos os Garotos que Já Amei' assume o papel de Sylvester Stallone em prelúdio que se passará na Guerra do Vietnã</subhead> | |
<body> | |
<p>De acordo com a <strong>Millennium Media</strong>, <strong>Noah Centineo</strong> foi escolhido para interpretar uma versão mais jovem de John Rambo no filme que contará os primórdios do lendário personagem. A produção, que é simplesmente chamada <em>John Rambo</em>, tenta examinar os primeiros anos do soldado antes dos eventos de <em>First Blood</em> (1982).</p> | |
<p><strong>Jalmari Helander</strong>, diretor finlandês mais conhecido pelo blockbuster de ação <em>Sisu</em>, comandará o filme. <strong>Rory Haines</strong> e <strong>Sohrab Noshirvani</strong>, que trabalharam juntos em <em>Black Adam</em>, estão cuidando do roteiro. As filmagens na Tailândia estão previstas para começar no início de 2026.</p> | |
<p>A história se passará durante a Guerra do Vietnã, embora os detalhes da trama estejam sendo mantidos em sigilo. O objetivo é retratar a metamorfose de John Rambo. Antes da guerra, ele era “o cara perfeito, o mais popular da escola, um superatleta”, como <strong>Sylvester Stallone</strong> afirmou em 2019. Espera-se que o filme examine os eventos horríveis que o moldaram no veterano atormentado retratado no primeiro filme.</p> | |
<p>Embora não esteja diretamente envolvido no projeto, <strong>Sylvester Stallone</strong>, que interpretou o personagem em cinco filmes, está ciente dele. Segundo pessoas próximas à produção, ele foi informado sobre a escolha de Centineo. O ator, hoje com 79 anos, brincou em 2023 sobre a possibilidade de voltar a interpretar o papel, dizendo: “Ele já fez praticamente tudo. O que eu vou combater? Artrite?”</p> | |
<p>A escolha de Centineo, de 29 anos, marca uma nova fase na carreira do ator, que conquistou fama internacional com comédias românticas da Netflix, como a trilogia <em>Para Todos os Garotos que Já Amei</em>. Nos últimos anos, porém, ele vem explorando o gênero de ação, interpretando o herói <em>Esmaga-Átomo</em> em <em>Adão Negro</em> e estrelando a série de espionagem <em>O Recruta</em>. Recentemente, Centineo também esteve no drama de guerra <em>Warfare</em>, da A24, e está escalado para viver Ken Masters no próximo filme de <em>Street Fighter</em>.</p> | |
<p>A franquia <em>Rambo</em>, baseada no livro <em>First Blood</em>, de David Morrell, é uma das mais conhecidas do cinema de ação. Os cinco filmes arrecadaram mais de 850 milhões de dólares em todo o mundo. Enquanto as sequências apostaram em ação em grande escala, o primeiro longa se destaca pelo tom mais solene e pela crítica ao tratamento dado aos veteranos do Vietnã.</p> | |
<p>A produção do novo filme está a cargo de <strong>Avi Lerner</strong>, <strong>Jonathan Yunger</strong>, <strong>Les Weldon</strong> e <strong>Kevin King-Templeton</strong>. A <strong>Lionsgate</strong>, que distribuiu os dois últimos longas da série, é a principal candidata a adquirir os direitos de distribuição do projeto.</p> | |
</body> | |
<instagram_title>Noah Centineo será o jovem Rambo em novo filme</instagram_title> | |
<instagram_description>Noah Centineo, famoso por “Para Todos os Garotos que Já Amei” e “O Recruta”, interpretará uma versão jovem de John Rambo no filme “John Rambo”, que narrará a origem do personagem interpretado por Sylvester Stallone.\n\nCom direção de Jalmari Helander (“Sisu”) e roteiro de Rory Haines e Sohrab Noshirvani (“Adão Negro”), o filme irá retratar o passado de Rambo antes de “First Blood” (1982), mostrando a transformação do famoso atleta escolar em um veterano marcado pela Guerra do Vietnã. As gravações na Tailândia têm início em 2026.\n\nStallone, que imortalizou o personagem em cinco filmes e arrecadou mais de 850 milhões de dólares na franquia, não participará diretamente do projeto, mas já foi informado sobre a escolha de Centineo.\n\nO prelúdio sinaliza uma mudança na trajetória do ator, que tem se estabelecido no cinema de ação com papéis em “Adão Negro”, “O Recruta” e “Warfare”.\n\nLeia mais sobre essa produção e seus bastidores no link da nossa bio.</instagram_description> | |
""" | |
config = types.GenerateContentConfig( | |
system_instruction=SYSTEM_INSTRUCTIONS, | |
thinking_config=types.ThinkingConfig( | |
thinking_budget=-1, | |
), | |
response_mime_type="text/plain", | |
max_output_tokens=4096, | |
temperature=0.8, | |
) | |
# Conteúdo da conversa | |
contents = [ | |
# Primeiro 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) | |
] | |
), | |
# Segundo exemplo | |
types.Content( | |
role="user", | |
parts=[ | |
types.Part.from_text(text=EXAMPLE_INPUT_2) | |
] | |
), | |
types.Content( | |
role="model", | |
parts=[ | |
types.Part.from_text(text=EXAMPLE_OUTPUT_2) | |
] | |
), | |
# Terceiro exemplo | |
types.Content( | |
role="user", | |
parts=[ | |
types.Part.from_text(text=EXAMPLE_INPUT_3) | |
] | |
), | |
types.Content( | |
role="model", | |
parts=[ | |
types.Part.from_text(text=EXAMPLE_OUTPUT_3) | |
] | |
), | |
# 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 | |
) | |
logger.info("Resposta do modelo recebida com sucesso") | |
# Extrair texto e fontes | |
response_text = extract_text_from_response(response) | |
sources = extract_sources_from_response(response) | |
logger.info(f"Texto extraído: {len(response_text) if response_text else 0} caracteres") | |
# Verificar se o texto está vazio | |
if not response_text or response_text.strip() == "": | |
logger.error("Texto extraído está vazio") | |
# Debug adicional: tentar logar a resposta crua | |
try: | |
logger.error(f"Resposta crua (primeiros 500 chars): {str(response)[:500]}") | |
except: | |
logger.error("Não foi possível converter resposta para string") | |
raise HTTPException( | |
status_code=500, | |
detail="Modelo não retornou conteúdo válido" | |
) | |
# Extração do título, subtítulo, conteúdo e campos do Instagram | |
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" | |
# Novos campos do Instagram | |
insta_title_match = re.search(r"<instagram_title>(.*?)</instagram_title>", response_text, re.DOTALL) | |
instagram_title = insta_title_match.group(1).strip() if insta_title_match else "Título Instagram não encontrado" | |
insta_desc_match = re.search(r"<instagram_description>(.*?)</instagram_description>", response_text, re.DOTALL) | |
instagram_description = insta_desc_match.group(1).strip() if insta_desc_match else "Descrição Instagram não encontrada" | |
logger.info(f"Processamento concluído com sucesso - Título: {title[:50]}...") | |
return NewsResponse( | |
title=title, | |
subhead=subhead, | |
content=content, | |
sources=sources, | |
instagram_title=instagram_title, | |
instagram_description=instagram_description | |
) | |
except HTTPException: | |
raise | |
except Exception as e: | |
logger.error(f"Erro na reescrita: {str(e)}") | |
raise HTTPException(status_code=500, detail=str(e)) |