File size: 15,003 Bytes
4ffe0a9
 
 
bbbc107
4ffe0a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bbbc107
4ffe0a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bbbc107
 
 
 
 
 
4ffe0a9
 
 
 
 
 
 
 
 
 
 
 
bbbc107
4ffe0a9
 
 
bbbc107
4ffe0a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bbbc107
4ffe0a9
 
 
 
 
 
 
 
bbbc107
4ffe0a9
 
 
 
bbbc107
 
 
 
4ffe0a9
bbbc107
 
 
4ffe0a9
bbbc107
 
4ffe0a9
 
 
bbbc107
4ffe0a9
 
 
 
bbbc107
4ffe0a9
 
bbbc107
4ffe0a9
 
 
 
 
 
 
 
 
 
 
bbbc107
4ffe0a9
 
 
bbbc107
4ffe0a9
 
 
 
 
 
bbbc107
4ffe0a9
 
 
 
 
 
bbbc107
4ffe0a9
 
 
 
 
 
bbbc107
4ffe0a9
 
 
 
 
 
 
 
bbbc107
4ffe0a9
 
 
 
 
 
 
bbbc107
4ffe0a9
 
bbbc107
4ffe0a9
 
 
 
bbbc107
 
 
4ffe0a9
 
 
 
 
 
 
 
bbbc107
4ffe0a9
 
 
bbbc107
4ffe0a9
bbbc107
4ffe0a9
 
bbbc107
4ffe0a9
bbbc107
 
 
4ffe0a9
 
 
 
 
bbbc107
4ffe0a9
 
bbbc107
4ffe0a9
 
 
bbbc107
 
4ffe0a9
bbbc107
4ffe0a9
 
bbbc107
4ffe0a9
bbbc107
4ffe0a9
 
 
 
 
 
 
 
 
 
 
 
bbbc107
4ffe0a9
 
 
 
 
 
 
 
 
bbbc107
4ffe0a9
 
 
 
 
 
 
bbbc107
4ffe0a9
 
 
 
 
 
 
 
 
 
 
 
bbbc107
4ffe0a9
 
 
bbbc107
 
4ffe0a9
 
 
 
bbbc107
4ffe0a9
 
 
 
 
 
bbbc107
4ffe0a9
 
 
 
 
 
 
bbbc107
4ffe0a9
 
bbbc107
4ffe0a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bbbc107
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
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

@router.post("/rewrite-news", response_model=NewsResponse)
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))