Skip to main content

game_manager_lib/services/integration/
gemini.rs

1//! Serviço para interagir com a API Gemini da Google para tradução de texto.
2//!
3//! Utiliza o modelo Gemini 2.5 para traduções de descrições de jogos.
4//!
5//! **Função Principal:**
6//! - `translate_text`: Traduz texto para português brasileiro mantendo termos técnicos de jogos em inglês.
7
8use crate::constants::GEMINI_API_URL;
9use crate::utils::http_client::HTTP_CLIENT;
10use serde::Deserialize;
11use serde_json::json;
12use tracing::{error, info};
13
14#[derive(Deserialize, Debug)]
15struct GeminiResponse {
16    candidates: Option<Vec<Candidate>>,
17    error: Option<GeminiError>, // Captura erros da API
18}
19
20#[derive(Deserialize, Debug)]
21struct GeminiError {
22    message: String,
23}
24
25#[derive(Deserialize, Debug)]
26struct Candidate {
27    content: Option<Content>,
28    #[serde(rename = "finishReason")]
29    finish_reason: Option<String>, // Útil para saber se foi bloqueado
30}
31
32#[derive(Deserialize, Debug)]
33struct Content {
34    parts: Vec<Part>,
35}
36
37#[derive(Deserialize, Debug)]
38struct Part {
39    text: String,
40}
41
42/// Traduz para português as descrições dos jogos
43///
44/// Mantém termos técnicos de jogos em inglês se forem comumente usados por gamers brasileiros.
45pub async fn translate_text(api_key: &str, text: &str) -> Result<String, String> {
46    info!("Iniciando tradução no Gemini (Modelo 2.5)...");
47
48    let url = format!("{}?key={}", GEMINI_API_URL, api_key);
49
50    let prompt = format!(
51        "Translate the following game description to Brazilian Portuguese (PT-BR). \
52        Maintain the tone (exciting, narrative). \
53        Keep technical gaming terms in English if they are commonly used by brazilian gamers (e.g., 'Roguelike', 'Metroidvania', 'Permadeath', 'Loot', 'Crafting'). \
54        Output ONLY the translated text, without preambles or markdown code blocks:\n\n{}",
55        text
56    );
57
58    // Adiciona Safety Settings para permitir descrições de jogos (Violência, etc)
59    let body = json!({
60        "contents": [{
61            "parts": [{
62                "text": prompt
63            }]
64        }],
65        "safetySettings": [
66            { "category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE" },
67            { "category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE" },
68            { "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE" },
69            { "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE" }
70        ]
71    });
72
73    let res = HTTP_CLIENT
74        .post(&url)
75        .json(&body)
76        .send()
77        .await
78        .map_err(|e| format!("Erro de rede Gemini: {}", e))?;
79
80    // Se o status não for 200, tentamos ler o corpo do erro para debug
81    if !res.status().is_success() {
82        let status = res.status();
83        let error_body = res.text().await.unwrap_or_default();
84        error!("Erro API Gemini ({}): {}", status, error_body);
85        return Err(format!(
86            "A API retornou erro {}: Verifique sua chave ou cota.",
87            status
88        ));
89    }
90
91    let data: GeminiResponse = res
92        .json()
93        .await
94        .map_err(|e| format!("Erro ao ler JSON Gemini: {}", e))?;
95
96    // Verifica se a API retornou um erro estruturado
97    if let Some(api_error) = data.error.as_ref() {
98        error!("Gemini API Error: {:?}", api_error);
99        return Err(format!("Gemini: {}", api_error.message));
100    }
101
102    // Tenta extrair o texto
103    if let Some(candidates) = data.candidates.as_ref() {
104        if let Some(first_candidate) = candidates.first() {
105            // Verifica se foi bloqueado por segurança (caso o BLOCK_NONE falhe)
106            if let Some(reason) = &first_candidate.finish_reason {
107                if reason != "STOP" {
108                    error!("Gemini bloqueou conteúdo. Motivo: {}", reason);
109                    return Err(format!(
110                        "Tradução bloqueada pelo filtro de segurança: {}",
111                        reason
112                    ));
113                }
114            }
115
116            if let Some(content) = &first_candidate.content {
117                if let Some(part) = content.parts.first() {
118                    info!("Tradução concluída com sucesso.");
119                    return Ok(part.text.trim().to_string());
120                }
121            }
122        }
123    }
124
125    error!("Resposta Gemini inesperada: {:?}", data);
126    Err("A IA não retornou nenhuma tradução válida.".to_string())
127}
128
129/// Traduz uma query de busca para inglês, se necessário.
130///
131/// Usado para normalizar buscas antes de enviar para APIs que só
132/// funcionam bem com termos em inglês (ex: GameBrain).
133///
134/// Se o texto já estiver em inglês, o modelo retorna sem alterações.
135pub async fn translate_query_to_english(api_key: &str, text: &str) -> Result<String, String> {
136    info!("Traduzindo query para inglês via Gemini...");
137
138    let url = format!("{}?key={}", GEMINI_API_URL, api_key);
139
140    let prompt = format!(
141        "If the following search query is not in English, translate it to English. \
142        If it is already in English, return it exactly as-is. \
143        Output ONLY the translated query, no explanations or punctuation:\n\n{}",
144        text
145    );
146
147    let body = json!({
148        "contents": [{
149            "parts": [{ "text": prompt }]
150        }]
151    });
152
153    let res = HTTP_CLIENT
154        .post(&url)
155        .json(&body)
156        .send()
157        .await
158        .map_err(|e| format!("Erro de rede Gemini: {}", e))?;
159
160    if !res.status().is_success() {
161        let status = res.status();
162        let error_body = res.text().await.unwrap_or_default();
163        error!(
164            "Erro API Gemini ao traduzir query ({}): {}",
165            status, error_body
166        );
167        // Falha silenciosa: usa a query original para não bloquear a busca
168        return Ok(text.to_string());
169    }
170
171    let data: GeminiResponse = res
172        .json()
173        .await
174        .map_err(|e| format!("Erro ao ler JSON Gemini: {}", e))?;
175
176    let translated = data
177        .candidates
178        .as_ref()
179        .and_then(|c| c.first())
180        .and_then(|c| c.content.as_ref())
181        .and_then(|c| c.parts.first())
182        .map(|p| p.text.trim().to_string());
183
184    match translated {
185        Some(t) if !t.is_empty() => {
186            info!("Query traduzida: '{}' -> '{}'", text, t);
187            Ok(t)
188        }
189        // Falha silenciosa: se Gemini não retornar nada, usa a query original
190        _ => {
191            error!("Gemini não retornou tradução para a query, usando original.");
192            Ok(text.to_string())
193        }
194    }
195}