habulaj commited on
Commit
bbbc107
·
verified ·
1 Parent(s): 05d2c32

Update routers/inference.py

Browse files
Files changed (1) hide show
  1. routers/inference.py +61 -297
routers/inference.py CHANGED
@@ -1,6 +1,7 @@
1
  import os
2
  import logging
3
  import json
 
4
  from fastapi import APIRouter, HTTPException
5
  from pydantic import BaseModel
6
  from google import genai
@@ -17,6 +18,7 @@ router = APIRouter()
17
 
18
  class NewsRequest(BaseModel):
19
  content: str
 
20
 
21
  class NewsResponse(BaseModel):
22
  title: str
@@ -32,12 +34,12 @@ def get_brazilian_date_string():
32
  try:
33
  # Tenta configurar o locale brasileiro
34
  locale_variants = [
35
- 'pt_BR.UTF-8', # Linux/macOS padrão
36
- 'pt_BR.utf8', # Algumas distribuições Linux
37
- 'pt_BR', # Fallback sem encoding
38
- 'Portuguese_Brazil.1252', # Windows
39
- 'Portuguese_Brazil', # Windows alternativo
40
- 'pt_BR.ISO8859-1', # Encoding alternativo
41
  ]
42
 
43
  locale_set = False
@@ -45,20 +47,16 @@ def get_brazilian_date_string():
45
  try:
46
  locale.setlocale(locale.LC_TIME, loc)
47
  locale_set = True
48
- logger.info(f"Locale configurado com sucesso: {loc}")
49
  break
50
  except locale.Error:
51
- logger.debug(f"Locale {loc} não disponível")
52
  continue
53
 
54
  if not locale_set:
55
- logger.warning("Nenhum locale brasileiro encontrado, usando locale padrão")
56
- locale.setlocale(locale.LC_TIME, '') # Usa locale padrão do sistema
57
 
58
- # Obtém a data atual no fuso horário de São Paulo
59
  now = datetime.now(ZoneInfo("America/Sao_Paulo"))
60
 
61
- # Dicionários para tradução manual (fallback caso locale não funcione)
62
  meses = {
63
  1: 'janeiro', 2: 'fevereiro', 3: 'março', 4: 'abril',
64
  5: 'maio', 6: 'junho', 7: 'julho', 8: 'agosto',
@@ -71,139 +69,100 @@ def get_brazilian_date_string():
71
  }
72
 
73
  try:
74
- # Tenta usar strftime com locale configurado
75
  if locale_set:
76
- # Para sistemas Unix/Linux usa %-d (sem zero à esquerda)
77
  try:
78
  date_string = now.strftime("%-d de %B de %Y (%A)")
79
  except ValueError:
80
- # Para Windows usa %#d
81
  try:
82
  date_string = now.strftime("%#d de %B de %Y (%A)")
83
  except ValueError:
84
- # Fallback: usa %d e remove zero manualmente se necessário
85
  date_string = now.strftime("%d de %B de %Y (%A)")
86
  if date_string.startswith('0'):
87
  date_string = date_string[1:]
88
 
89
- # Força dia da semana em minúscula
90
  date_string = date_string.replace(date_string.split('(')[1].split(')')[0],
91
  date_string.split('(')[1].split(')')[0].lower())
92
  else:
93
- # Fallback manual completo
94
  dia = now.day
95
  mes = meses[now.month]
96
  ano = now.year
97
  dia_semana = dias_semana[now.weekday()]
98
  date_string = f"{dia} de {mes} de {ano} ({dia_semana})"
99
 
100
- except Exception as e:
101
- logger.warning(f"Erro ao formatar data com locale: {e}")
102
- # Fallback manual completo em caso de erro
103
  dia = now.day
104
  mes = meses[now.month]
105
  ano = now.year
106
  dia_semana = dias_semana[now.weekday()]
107
  date_string = f"{dia} de {mes} de {ano} ({dia_semana})"
108
 
109
- logger.info(f"Data detectada e formatada: {date_string}")
110
  return date_string
111
 
112
- except Exception as e:
113
- logger.error(f"Erro crítico na formatação de data: {e}")
114
- # Último fallback: data em inglês
115
  now = datetime.now(ZoneInfo("America/Sao_Paulo"))
116
  date_string = now.strftime("%d de %B de %Y")
117
- logger.warning(f"Usando fallback de data: {date_string}")
118
  return date_string
119
 
120
- def safe_json_serialize(obj):
121
- """Converte objeto em JSON de forma segura para logging"""
 
 
122
  try:
123
- return json.dumps(obj, indent=2, ensure_ascii=False, default=str)
 
 
124
  except Exception as e:
125
- logger.warning(f"Erro ao serializar objeto para JSON: {e}")
126
- return str(obj)
127
 
128
  def extract_text_from_response(response):
129
  """
130
- Extrai o texto da resposta de forma robusta, lidando com diferentes estruturas possíveis.
131
  """
132
  response_text = ""
133
 
134
- # Método 1: Tentar response.text diretamente
135
  if hasattr(response, 'text') and response.text:
136
- response_text = response.text
137
- logger.info(f"Texto extraído via response.text: {len(response_text)} caracteres")
138
- return response_text
139
 
140
- # Método 2: Tentar extrair dos candidates
141
  if hasattr(response, 'candidates') and response.candidates:
142
- for i, candidate in enumerate(response.candidates):
143
- logger.debug(f"Processando candidate {i}")
144
-
145
  if not hasattr(candidate, 'content') or not candidate.content:
146
- logger.debug(f"Candidate {i} não tem content válido")
147
  continue
148
 
149
  content = candidate.content
150
 
151
- # Verificar se content tem parts e se não é None
152
  if not hasattr(content, 'parts') or content.parts is None:
153
- logger.debug(f"Candidate {i} content.parts é None ou não existe")
154
  continue
155
 
156
- # Iterar pelas parts de forma segura
157
  try:
158
  parts_list = list(content.parts) if content.parts else []
159
- logger.debug(f"Candidate {i} tem {len(parts_list)} parts")
160
 
161
- for j, part in enumerate(parts_list):
162
  if hasattr(part, 'text') and part.text:
163
  response_text += part.text
164
- logger.debug(f"Texto adicionado do candidate {i}, part {j}: {len(part.text)} chars")
165
 
166
- except Exception as e:
167
- logger.warning(f"Erro ao processar parts do candidate {i}: {e}")
168
  continue
169
 
170
- # Método 3: Tentar atributos alternativos
171
- if not response_text and hasattr(response, 'candidates'):
172
- for i, candidate in enumerate(response.candidates):
173
- for attr_name in ['output', 'generated_text', 'text_content']:
174
- if hasattr(candidate, attr_name):
175
- attr_value = getattr(candidate, attr_name)
176
- if attr_value and str(attr_value).strip():
177
- response_text += str(attr_value)
178
- logger.info(f"Texto encontrado em candidate.{attr_name}")
179
- break
180
-
181
- logger.info(f"Texto total extraído: {len(response_text)} caracteres")
182
  return response_text
183
 
184
  def extract_sources_from_response(response):
185
  """
186
- Extrai as fontes (URLs) do grounding metadata de forma robusta.
187
  """
188
  sources = []
189
 
190
  if not (hasattr(response, 'candidates') and response.candidates):
191
- logger.debug("Response não tem candidates")
192
  return sources
193
 
194
- for i, candidate in enumerate(response.candidates):
195
  if not (hasattr(candidate, 'grounding_metadata') and candidate.grounding_metadata):
196
- logger.debug(f"Candidate {i} não tem grounding_metadata")
197
  continue
198
 
199
  grounding_metadata = candidate.grounding_metadata
200
- logger.debug(f"Processando grounding_metadata do candidate {i}")
201
 
202
- # Verificar grounding_chunks
203
  if hasattr(grounding_metadata, 'grounding_chunks') and grounding_metadata.grounding_chunks:
204
- logger.debug(f"Encontrados {len(grounding_metadata.grounding_chunks)} grounding_chunks")
205
-
206
- for j, chunk in enumerate(grounding_metadata.grounding_chunks):
207
  try:
208
  if (hasattr(chunk, 'web') and chunk.web and
209
  hasattr(chunk.web, 'uri') and chunk.web.uri):
@@ -211,90 +170,69 @@ def extract_sources_from_response(response):
211
  uri = chunk.web.uri
212
  if uri and uri not in sources:
213
  sources.append(uri)
214
- logger.debug(f"Fonte adicionada do chunk {j}: {uri}")
215
 
216
- except Exception as e:
217
- logger.warning(f"Erro ao processar chunk {j}: {e}")
218
  continue
219
-
220
- # Verificar search_entry_point (método alternativo)
221
- if hasattr(grounding_metadata, 'search_entry_point') and grounding_metadata.search_entry_point:
222
- logger.debug("Search entry point encontrado")
223
- # Aqui você pode adicionar lógica adicional se necessário
224
 
225
- logger.info(f"Total de fontes únicas encontradas: {len(sources)}")
226
  return sources
227
 
228
  @router.post("/rewrite-news", response_model=NewsResponse)
229
  async def rewrite_news(news: NewsRequest):
230
  """
231
- Endpoint para reescrever notícias usando o modelo Gemini.
232
  """
233
  try:
234
- logger.info(f"Iniciando reescrita de notícia com {len(news.content)} caracteres")
235
-
236
- # Verificar se a API key está configurada
237
  api_key = os.environ.get("GEMINI_API_KEY")
238
  if not api_key:
239
- logger.error("GEMINI_API_KEY não encontrada nas variáveis de ambiente")
240
  raise HTTPException(status_code=500, detail="API key não configurada")
241
 
 
 
 
242
  client = genai.Client(api_key=api_key)
243
  model = "gemini-2.5-pro"
244
 
245
  # Obter data formatada
246
  date_string = get_brazilian_date_string()
247
- logger.info(f"Usando data: {date_string}")
248
 
249
  # Instruções do sistema
250
  SYSTEM_INSTRUCTIONS = f"""
251
- Você é um jornalista de entretenimento brasileiro, escrevendo para portais digitais modernos. Sua missão é transformar notícias internacionais em matérias originais, atualizadas e envolventes para o público brasileiro.
252
 
253
  ESTILO DE ESCRITA:
254
 
255
- - Tom natural, direto e conversacional, sem frases feitas ou jargões jornalísticos
256
  - 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.
257
- - Sem linguagem militante ou juízos morais: mantenha tom informativo e predominantemente conservador
258
  - Evite opiniões genéricas; foque em fatos e contexto
259
 
260
- CONTEÚDO E ABORDAGEM:
261
 
262
- - Use a notícia-base como ponto de partida, não como roteiro
263
- - PESQUISE SEMPRE usando a ferramenta de busca, Variety, THR, Deadline e fontes confiáveis em inglês
264
- - Traga dados extras, contexto histórico e informações relevantes
265
- - Sempre que possível, inclua títulos de obras em Português do Brasil
266
-
267
- ESTRUTURA E FORMATAÇÃO:
268
-
269
- - Comece com um lead que já entrega o essencial
270
- - Desenvolva com profundidade, conectando com outros fatos e entregando contexto completo, principalmente no cenário brasileiro
271
- - 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
272
  - Otimize para leitura digital e SEO (parágrafos bem segmentados, palavras chaves, etc)
273
 
274
  FORMATO:
275
 
276
  <headline>título aqui</headline>
277
- <subhead>subtítulo aqui</subhead>
278
  <body>conteúdo aqui</body>
279
 
280
- Use <strong> para destaques e <em> para títulos de obras ou citações. Apenas. Mais tags estão estritamente proibidas.
281
 
282
  TÍTULOS:
283
 
284
- - Padrão brasileiro: só a primeira palavra em maiúscula (exceto nomes próprios e títulos de filmes/séries)
285
- - Seja claro, direto e específico... Se houver, não inclua spoilers no título, apenas no body
286
-
287
- OBRIGAÇÕES:
288
 
289
- 1. É obrigatório usar a ferramenta de busca para complementar a notícia se no prompt, contiver /google.
290
- 2. Traga sempre informações novas e contextualizadas.
291
-
292
- O resultado deve soar como uma matéria escrita por um profissional experiente, não por IA. Seja preciso, atual e interessante. Sempre pesquise informações adicionais se no prompt contiver /google, isso é obrigatório. Sempre complete a notícia com acontecimentos que se ligam, sempre contextualize tudo para o leitor. A data de hoje é {date_string}
293
  """
294
 
295
- # Primeiro exemplo
296
  EXAMPLE_INPUT_1 = """
297
- /google 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.
298
  """
299
 
300
  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>
@@ -307,68 +245,7 @@ O resultado deve soar como uma matéria escrita por um profissional experiente,
307
  <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>
308
  </body>"""
309
 
310
- # Segundo exemplo
311
- EXAMPLE_INPUT_2 = """
312
- /google News base: The Office spinoff series 'The Paper' has set a September premiere date at Peacock.
313
-
314
- 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.
315
-
316
- '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.
317
-
318
- '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.
319
-
320
- Guest stars for the show include Eric Rahill, Tracy Letts, Molly Ephraim, Mo Welch, Allan Havey, Duane Shepard Sr., Nate Jackson and Nancy Lenehan.
321
-
322
- '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).
323
-
324
- 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.
325
-
326
- '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.
327
- """
328
-
329
- EXAMPLE_OUTPUT_2 = """<headline>Nova série do universo 'The Office' ganha título, data de estreia e um rosto familiar</headline>
330
- <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>
331
- <body>
332
- <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>
333
- <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>
334
- <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>
335
- <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>
336
- <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>
337
- <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>
338
- <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>
339
- <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>
340
- </body>"""
341
-
342
- # Terceiro exemplo
343
- EXAMPLE_INPUT_3 = """
344
- /google News base: The first trailer for "Eyes of Wakanda," a four-episode limited animated series executive produced by "Black Panther" director Ryan Coogler, has been released. It will release on Disney+ on Aug. 1.
345
-
346
- The upcoming show will be set in Wakanda, the home of the Black Panther that was extensively explored in Coogler's two Marvel movies. The show will follow "the adventures of brave Wakandan warriors throughout history. In this globe-trotting adventure, the heroes must carry-out dangerous missions to retrieve Vibranium artifacts from the enemies of Wakanda. They are the Hatut Zaraze and this is their story."
347
-
348
- The voice cast includes Winnie Harlow, Cress Williams, Patricia Belcher, Larry Herron, Adam Gold, Lynn Whitfield, Jacques Colimon, Jona Xiao, Isaac Robinson-Smith, Gary Anthony Williams, Zeke Alton, Steve Toussaint and Anika Noni Rose.
349
-
350
- Popular on Variety
351
-
352
- The last time fans saw Wakanda was in 2022's "Black Panther: Wakanda Forever," the sequel to Marvel's 2018 best picture nominee. That movie introduced the underwater world of Atlantis and featured the returns of Letitia Wright, Lupita Nyong'o, Angela Bassett, Winston Duke, Danai Gurira and Martin Freeman. Wakandans have popped up in other places in the MCU, including an appearance by Ayo, played by Florence Kasumba, in the Disney+ series "The Falcon and The Winter Soldier." The second-in-command of the Dora Milaje, Ayo first appeared in "Captain America: Civil War."
353
-
354
- The "Wakanda" series is the first in a multi-year overall TV deal with Coogler's Proximity production company and The Walt Disney Company, with other TV projects in currently in development for the MCU.
355
-
356
- "Eyes of Wakanda" is helmed by director/executive producer Todd Harris, who was first a storyboard artist on "Black Panther" and "Black Panther: Wakanda Forever" then as an illustrator on "Sinners" before directing "Eyes of Wakanda." The show is executive produced by Coogler, Brad Winderbaum, Kevin Feige, Louis D'Esposito and Dana Vasquez-Eberhardt.
357
-
358
- The article is out of date, more information is needed.
359
- """
360
-
361
- EXAMPLE_OUTPUT_3 = """<headline>Olhos de Wakanda: nova animação da Marvel ganha trailer e data de estreia</headline>
362
- <subhead>A série de Ryan Coogler vai mergulhar na história do reino de T'Challa e nas missões secretas de seus guerreiros para proteger o vibranium.</subhead>
363
- <body>
364
- <p>A <strong>Marvel Studios</strong> acaba de publicar o primeiro trailer da sua próxima série animada, <em>Olhos de Wakanda</em>, que explora a história do avançado reino africano. Foi anunciado que a série terá produção executiva de <strong>Ryan Coogler</strong>, diretor dos filmes do <strong>Pantera Negra</strong>, e será lançada como uma minissérie em quatro partes no dia <strong>1º de agosto</strong> no <strong>Disney Plus</strong>. A data é um pouco surpreendente, pois o lançamento estava inicialmente marcado para <strong>27 de agosto</strong>.</p>
365
- <p>A narrativa acompanhará as façanhas dos <strong>Hatut Zaraze</strong>, os guerreiros de elite de <strong>Wakanda</strong>, enquanto embarcam em missões globais para recuperar artefatos de <strong>vibranium</strong> roubados. Conhecida nos quadrinhos como os <em>"Cães de Guerra"</em>, essa força secreta atua como uma agência clandestina, protegendo os recursos e inovações ocultas da nação. Os ancestrais de <strong>T'Challa</strong> serão ainda mais explorados na série, mostrando como eles, junto de seus descendentes, forjaram o legado do <strong>Pantera Negra</strong>, que ele um dia herdará, ao longo de séculos de defesa, em arcos cronológicos marcantes.</p>
366
- <p><strong>Todd Harris</strong>, que trabalhou como artista de storyboard nos filmes da franquia, criou e dirigiu a obra, que terá um visual distinto, separado das adaptações. Segundo Harris, a trama integra espionagem com eventos históricos, começando no final da <strong>Idade do Bronze</strong> e se estendendo por séculos. O diretor acrescentou: <em>"Você tem um James Bond com a qualidade de Wakanda e às vezes uma Jane Bond com toda a grandiosidade visual e cultural que a nação representa."</em></p>
367
- <p><strong>Winnie Harlow</strong>, <strong>Cress Williams</strong>, <strong>Patricia Belcher</strong>, <strong>Lynn Whitfield</strong> e <strong>Anika Noni Rose</strong> fazem parte do elenco de vozes. Além de expandir a mitologia de <strong>Wakanda</strong> dentro do <strong>MCU</strong>, a série também apresentará uma versão ancestral do herói <strong>Punho de Ferro</strong>. <strong>Brad Winderbaum</strong>, chefe de TV e streaming da <strong>Marvel Studios</strong>, confirmou que <em>Olhos de Wakanda</em> estará profundamente integrada com a linha do tempo principal do <strong>MCU</strong>.</p>
368
- <p>A produtora de <strong>Coogler</strong>, <strong>Proximity Media</strong>, e a <strong>Disney</strong> têm um acordo televisivo de vários anos que inclui a produção de outros projetos do <strong>MCU</strong>. No dia da estreia, todos os quatro episódios de <em>Olhos de Wakanda</em> estarão disponíveis ao mesmo tempo.</p>
369
- </body>"""
370
-
371
- # Configuração correta da ferramenta de pesquisa
372
  grounding_tool = types.Tool(
373
  google_search=types.GoogleSearch()
374
  )
@@ -378,7 +255,7 @@ The article is out of date, more information is needed.
378
  thinking_config=types.ThinkingConfig(
379
  thinking_budget=-1,
380
  ),
381
- tools=[grounding_tool], # Use a ferramenta corretamente
382
  response_mime_type="text/plain",
383
  max_output_tokens=4096,
384
  temperature=0.8,
@@ -386,145 +263,54 @@ The article is out of date, more information is needed.
386
 
387
  # Conteúdo da conversa
388
  contents = [
389
- # Primeiro exemplo: usuário envia uma notícia
390
  types.Content(
391
  role="user",
392
  parts=[
393
  types.Part.from_text(text=EXAMPLE_INPUT_1)
394
  ]
395
  ),
396
- # Primeiro exemplo: modelo responde com o formato correto
397
  types.Content(
398
  role="model",
399
  parts=[
400
  types.Part.from_text(text=EXAMPLE_OUTPUT_1)
401
  ]
402
  ),
403
- # Segundo exemplo: usuário envia outra notícia
404
- types.Content(
405
- role="user",
406
- parts=[
407
- types.Part.from_text(text=EXAMPLE_INPUT_2)
408
- ]
409
- ),
410
- # Segundo exemplo: modelo responde com o formato correto
411
- types.Content(
412
- role="model",
413
- parts=[
414
- types.Part.from_text(text=EXAMPLE_OUTPUT_2)
415
- ]
416
- ),
417
- # Terceiro exemplo: usuário envia outra notícia
418
  types.Content(
419
  role="user",
420
  parts=[
421
- types.Part.from_text(text=EXAMPLE_INPUT_3)
422
- ]
423
- ),
424
- # Terceiro exemplo: modelo responde com o formato correto
425
- types.Content(
426
- role="model",
427
- parts=[
428
- types.Part.from_text(text=EXAMPLE_OUTPUT_3)
429
- ]
430
- ),
431
- # Agora o usuário envia a notícia real para ser reescrita
432
- types.Content(
433
- role="user",
434
- parts=[
435
- types.Part.from_text(text=f"/google News base: {news.content}. The article is out of date, more information is needed.")
436
  ]
437
  )
438
  ]
439
 
440
- logger.info("Iniciando geração de conteúdo com o modelo Gemini")
441
-
442
- # Usar generate_content em vez de generate_content_stream para melhor controle do grounding
443
  response = client.models.generate_content(
444
  model=model,
445
  contents=contents,
446
  config=config
447
  )
448
 
449
- # =================================
450
- # LOGS DETALHADOS DA RESPOSTA BRUTA
451
- # =================================
452
- logger.info("=== RESPOSTA BRUTA COMPLETA DO GEMINI ===")
453
- logger.info(f"Tipo da resposta: {type(response)}")
454
- logger.info(f"Resposta completa: {safe_json_serialize(response)}")
455
-
456
- # Log dos atributos principais
457
- logger.info(f"response.text: '{response.text if hasattr(response, 'text') else 'ATRIBUTO text NÃO EXISTE'}'")
458
- logger.info(f"response.candidates: {safe_json_serialize(response.candidates) if hasattr(response, 'candidates') else 'ATRIBUTO candidates NÃO EXISTE'}")
459
-
460
- # Log detalhado dos candidates - COM PROTEÇÃO CONTRA PARTS = None
461
- if hasattr(response, 'candidates') and response.candidates:
462
- for i, candidate in enumerate(response.candidates):
463
- logger.info(f"=== CANDIDATE {i} ===")
464
- logger.info(f"Candidate completo: {safe_json_serialize(candidate)}")
465
-
466
- if hasattr(candidate, 'content'):
467
- logger.info(f"Candidate.content: {safe_json_serialize(candidate.content)}")
468
-
469
- if hasattr(candidate.content, 'parts'):
470
- if candidate.content.parts is None:
471
- logger.warning(f"ATENÇÃO: Candidate {i} content.parts é None!")
472
- else:
473
- try:
474
- parts_list = list(candidate.content.parts)
475
- logger.info(f"Candidate {i} tem {len(parts_list)} parts")
476
-
477
- for j, part in enumerate(parts_list):
478
- logger.info(f"Part {j}: {safe_json_serialize(part)}")
479
- if hasattr(part, 'text'):
480
- logger.info(f"Part {j} text: '{part.text}'")
481
-
482
- except Exception as e:
483
- logger.error(f"Erro ao processar parts do candidate {i}: {e}")
484
- else:
485
- logger.warning(f"Candidate {i} content não tem atributo 'parts'")
486
-
487
- if hasattr(candidate, 'finish_reason'):
488
- logger.info(f"Finish reason: {candidate.finish_reason}")
489
-
490
- if hasattr(candidate, 'safety_ratings'):
491
- logger.info(f"Safety ratings: {safe_json_serialize(candidate.safety_ratings)}")
492
-
493
- if hasattr(candidate, 'grounding_metadata'):
494
- logger.info(f"Grounding metadata: {safe_json_serialize(candidate.grounding_metadata)}")
495
-
496
- # Log de outros atributos possíveis
497
- for attr in ['usage_metadata', 'prompt_feedback', 'model_version']:
498
- if hasattr(response, attr):
499
- logger.info(f"response.{attr}: {safe_json_serialize(getattr(response, attr))}")
500
-
501
- logger.info("=== FIM DA RESPOSTA BRUTA ===")
502
-
503
- # Usar as funções seguras para extração
504
  response_text = extract_text_from_response(response)
505
  sources = extract_sources_from_response(response)
506
-
507
- logger.info(f"Resposta do modelo recebida com {len(response_text)} caracteres")
508
- logger.info(f"Encontradas {len(sources)} fontes únicas: {sources}")
509
 
510
  # Verificar se o texto está vazio
511
  if not response_text or response_text.strip() == "":
512
- logger.error("ERRO CRÍTICO: Texto da resposta está vazio!")
513
  raise HTTPException(
514
  status_code=500,
515
- detail="Modelo não retornou conteúdo válido. Verifique os logs do servidor."
516
  )
517
 
518
- # Extração do título, subtítulo e conteúdo usando as tags
519
  title_match = re.search(r"<headline>(.*?)</headline>", response_text, re.DOTALL)
520
  title = title_match.group(1).strip() if title_match else "Título não encontrado"
521
- logger.info(f"Título extraído: '{title}'")
522
 
523
  subhead_match = re.search(r"<subhead>(.*?)</subhead>", response_text, re.DOTALL)
524
  subhead = subhead_match.group(1).strip() if subhead_match else "Subtítulo não encontrado"
525
- logger.info(f"Subtítulo extraído: '{subhead}'")
526
 
527
- # Extração do conteúdo com lógica mais flexível para <body>
528
  body_match = re.search(r"<body>(.*?)</body>", response_text, re.DOTALL)
529
  if body_match:
530
  content = body_match.group(1).strip()
@@ -534,33 +320,11 @@ The article is out of date, more information is needed.
534
  content = body_start_match.group(1).strip()
535
  else:
536
  content = "Conteúdo não encontrado"
537
-
538
- logger.info(f"Conteúdo extraído: {len(content)} caracteres - '{content[:200]}...' (primeiros 200 chars)")
539
-
540
- # Verificações adicionais
541
- if title == "Título não encontrado" or subhead == "Subtítulo não encontrado" or content == "Conteúdo não encontrado":
542
- logger.warning("Algumas partes do conteúdo não foram encontradas!")
543
- logger.warning(f"Texto completo da resposta: '{response_text}'")
544
-
545
- logger.info("Artigo reescrito com sucesso")
546
- logger.debug(f"Título extraído: {title[:50]}...")
547
- logger.debug(f"Subtítulo extraído: {subhead[:50]}...")
548
- logger.debug(f"Conteúdo extraído: {len(content)} caracteres")
549
- logger.debug(f"Sources encontradas: {sources}")
550
-
551
- # Log adicional para debug
552
- logger.info("=== RESULTADO FINAL ===")
553
- logger.info(f"Title: '{title}'")
554
- logger.info(f"Subhead: '{subhead}'")
555
- logger.info(f"Content length: {len(content)}")
556
- logger.info(f"Sources count: {len(sources)}")
557
 
558
  return NewsResponse(title=title, subhead=subhead, content=content, sources=sources)
559
 
560
  except HTTPException:
561
- # Re-lança HTTPExceptions para manter o status code correto
562
  raise
563
  except Exception as e:
564
- error_msg = f"Erro na reescrita: {str(e)}"
565
- logger.error(error_msg, exc_info=True)
566
- raise HTTPException(status_code=500, detail=error_msg)
 
1
  import os
2
  import logging
3
  import json
4
+ import requests
5
  from fastapi import APIRouter, HTTPException
6
  from pydantic import BaseModel
7
  from google import genai
 
18
 
19
  class NewsRequest(BaseModel):
20
  content: str
21
+ sources_url: str # URL do arquivo fontes.txt
22
 
23
  class NewsResponse(BaseModel):
24
  title: str
 
34
  try:
35
  # Tenta configurar o locale brasileiro
36
  locale_variants = [
37
+ 'pt_BR.UTF-8',
38
+ 'pt_BR.utf8',
39
+ 'pt_BR',
40
+ 'Portuguese_Brazil.1252',
41
+ 'Portuguese_Brazil',
42
+ 'pt_BR.ISO8859-1',
43
  ]
44
 
45
  locale_set = False
 
47
  try:
48
  locale.setlocale(locale.LC_TIME, loc)
49
  locale_set = True
 
50
  break
51
  except locale.Error:
 
52
  continue
53
 
54
  if not locale_set:
55
+ locale.setlocale(locale.LC_TIME, '')
 
56
 
 
57
  now = datetime.now(ZoneInfo("America/Sao_Paulo"))
58
 
59
+ # Dicionários para tradução manual (fallback)
60
  meses = {
61
  1: 'janeiro', 2: 'fevereiro', 3: 'março', 4: 'abril',
62
  5: 'maio', 6: 'junho', 7: 'julho', 8: 'agosto',
 
69
  }
70
 
71
  try:
 
72
  if locale_set:
 
73
  try:
74
  date_string = now.strftime("%-d de %B de %Y (%A)")
75
  except ValueError:
 
76
  try:
77
  date_string = now.strftime("%#d de %B de %Y (%A)")
78
  except ValueError:
 
79
  date_string = now.strftime("%d de %B de %Y (%A)")
80
  if date_string.startswith('0'):
81
  date_string = date_string[1:]
82
 
 
83
  date_string = date_string.replace(date_string.split('(')[1].split(')')[0],
84
  date_string.split('(')[1].split(')')[0].lower())
85
  else:
 
86
  dia = now.day
87
  mes = meses[now.month]
88
  ano = now.year
89
  dia_semana = dias_semana[now.weekday()]
90
  date_string = f"{dia} de {mes} de {ano} ({dia_semana})"
91
 
92
+ except Exception:
 
 
93
  dia = now.day
94
  mes = meses[now.month]
95
  ano = now.year
96
  dia_semana = dias_semana[now.weekday()]
97
  date_string = f"{dia} de {mes} de {ano} ({dia_semana})"
98
 
 
99
  return date_string
100
 
101
+ except Exception:
 
 
102
  now = datetime.now(ZoneInfo("America/Sao_Paulo"))
103
  date_string = now.strftime("%d de %B de %Y")
 
104
  return date_string
105
 
106
+ def download_sources_file(url: str) -> str:
107
+ """
108
+ Baixa o arquivo fontes.txt da URL fornecida.
109
+ """
110
  try:
111
+ response = requests.get(url, timeout=30)
112
+ response.raise_for_status()
113
+ return response.text
114
  except Exception as e:
115
+ logger.error(f"Erro ao baixar arquivo de fontes: {e}")
116
+ raise HTTPException(status_code=400, detail=f"Erro ao baixar arquivo de fontes: {str(e)}")
117
 
118
  def extract_text_from_response(response):
119
  """
120
+ Extrai o texto da resposta de forma robusta.
121
  """
122
  response_text = ""
123
 
 
124
  if hasattr(response, 'text') and response.text:
125
+ return response.text
 
 
126
 
 
127
  if hasattr(response, 'candidates') and response.candidates:
128
+ for candidate in response.candidates:
 
 
129
  if not hasattr(candidate, 'content') or not candidate.content:
 
130
  continue
131
 
132
  content = candidate.content
133
 
 
134
  if not hasattr(content, 'parts') or content.parts is None:
 
135
  continue
136
 
 
137
  try:
138
  parts_list = list(content.parts) if content.parts else []
 
139
 
140
+ for part in parts_list:
141
  if hasattr(part, 'text') and part.text:
142
  response_text += part.text
 
143
 
144
+ except Exception:
 
145
  continue
146
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  return response_text
148
 
149
  def extract_sources_from_response(response):
150
  """
151
+ Extrai as fontes (URLs) do grounding metadata.
152
  """
153
  sources = []
154
 
155
  if not (hasattr(response, 'candidates') and response.candidates):
 
156
  return sources
157
 
158
+ for candidate in response.candidates:
159
  if not (hasattr(candidate, 'grounding_metadata') and candidate.grounding_metadata):
 
160
  continue
161
 
162
  grounding_metadata = candidate.grounding_metadata
 
163
 
 
164
  if hasattr(grounding_metadata, 'grounding_chunks') and grounding_metadata.grounding_chunks:
165
+ for chunk in grounding_metadata.grounding_chunks:
 
 
166
  try:
167
  if (hasattr(chunk, 'web') and chunk.web and
168
  hasattr(chunk.web, 'uri') and chunk.web.uri):
 
170
  uri = chunk.web.uri
171
  if uri and uri not in sources:
172
  sources.append(uri)
 
173
 
174
+ except Exception:
 
175
  continue
 
 
 
 
 
176
 
 
177
  return sources
178
 
179
  @router.post("/rewrite-news", response_model=NewsResponse)
180
  async def rewrite_news(news: NewsRequest):
181
  """
182
+ Endpoint para reescrever notícias usando o modelo Gemini com arquivo de fontes.
183
  """
184
  try:
185
+ # Verificar API key
 
 
186
  api_key = os.environ.get("GEMINI_API_KEY")
187
  if not api_key:
 
188
  raise HTTPException(status_code=500, detail="API key não configurada")
189
 
190
+ # Baixar arquivo de fontes
191
+ sources_content = download_sources_file(news.sources_url)
192
+
193
  client = genai.Client(api_key=api_key)
194
  model = "gemini-2.5-pro"
195
 
196
  # Obter data formatada
197
  date_string = get_brazilian_date_string()
 
198
 
199
  # Instruções do sistema
200
  SYSTEM_INSTRUCTIONS = f"""
201
+ 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.
202
 
203
  ESTILO DE ESCRITA:
204
 
205
+ - Tom natural, direto e conversacional, sem frases feitas ou jargões jornalísticos.
206
  - 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.
207
+ - Sem linguagem militante ou juízos morais: mantenha tom informativo e predominantemente conservador / aspectro de Direita que o jornal tem.
208
  - Evite opiniões genéricas; foque em fatos e contexto
209
 
210
+ CONTEÚDO, ESTRUTURA E ABORDAGEM:
211
 
212
+ - 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.
213
+ - Sempre que possível, inclua títulos de obras em Português do Brasil.
214
+ - 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.
 
 
 
 
 
 
 
215
  - Otimize para leitura digital e SEO (parágrafos bem segmentados, palavras chaves, etc)
216
 
217
  FORMATO:
218
 
219
  <headline>título aqui</headline>
220
+ <subhead>subtítulo aqui</subhead>
221
  <body>conteúdo aqui</body>
222
 
223
+ Use <strong> para destaques e <em> para títulos de obras ou citações.
224
 
225
  TÍTULOS:
226
 
227
+ - Padrão brasileiro: só a primeira palavra em maiúscula (exceto nomes próprios e títulos de filmes/séries/obras, etc.)
228
+ - Seja claro, direto e específico... Se houver, não inclua spoilers no título, apenas no body.
 
 
229
 
230
+ 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}
 
 
 
231
  """
232
 
233
+ # Exemplos (mantidos os mesmos do código original)
234
  EXAMPLE_INPUT_1 = """
235
+ 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.
236
  """
237
 
238
  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>
 
245
  <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>
246
  </body>"""
247
 
248
+ # Configuração da ferramenta de pesquisa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  grounding_tool = types.Tool(
250
  google_search=types.GoogleSearch()
251
  )
 
255
  thinking_config=types.ThinkingConfig(
256
  thinking_budget=-1,
257
  ),
258
+ tools=[grounding_tool],
259
  response_mime_type="text/plain",
260
  max_output_tokens=4096,
261
  temperature=0.8,
 
263
 
264
  # Conteúdo da conversa
265
  contents = [
266
+ # Exemplo
267
  types.Content(
268
  role="user",
269
  parts=[
270
  types.Part.from_text(text=EXAMPLE_INPUT_1)
271
  ]
272
  ),
 
273
  types.Content(
274
  role="model",
275
  parts=[
276
  types.Part.from_text(text=EXAMPLE_OUTPUT_1)
277
  ]
278
  ),
279
+ # Notícia atual com arquivo de fontes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  types.Content(
281
  role="user",
282
  parts=[
283
+ types.Part.from_text(text=f"News base: {news.content}. The article is out of date, more information is needed."),
284
+ types.Part.from_text(text=f"Fontes adicionais disponíveis:\n\n{sources_content}")
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  ]
286
  )
287
  ]
288
 
289
+ # Gerar conteúdo
 
 
290
  response = client.models.generate_content(
291
  model=model,
292
  contents=contents,
293
  config=config
294
  )
295
 
296
+ # Extrair texto e fontes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  response_text = extract_text_from_response(response)
298
  sources = extract_sources_from_response(response)
 
 
 
299
 
300
  # Verificar se o texto está vazio
301
  if not response_text or response_text.strip() == "":
 
302
  raise HTTPException(
303
  status_code=500,
304
+ detail="Modelo não retornou conteúdo válido"
305
  )
306
 
307
+ # Extração do título, subtítulo e conteúdo
308
  title_match = re.search(r"<headline>(.*?)</headline>", response_text, re.DOTALL)
309
  title = title_match.group(1).strip() if title_match else "Título não encontrado"
 
310
 
311
  subhead_match = re.search(r"<subhead>(.*?)</subhead>", response_text, re.DOTALL)
312
  subhead = subhead_match.group(1).strip() if subhead_match else "Subtítulo não encontrado"
 
313
 
 
314
  body_match = re.search(r"<body>(.*?)</body>", response_text, re.DOTALL)
315
  if body_match:
316
  content = body_match.group(1).strip()
 
320
  content = body_start_match.group(1).strip()
321
  else:
322
  content = "Conteúdo não encontrado"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
 
324
  return NewsResponse(title=title, subhead=subhead, content=content, sources=sources)
325
 
326
  except HTTPException:
 
327
  raise
328
  except Exception as e:
329
+ logger.error(f"Erro na reescrita: {str(e)}")
330
+ raise HTTPException(status_code=500, detail=str(e))