game_manager_lib/services/integration/
gemini.rs1use 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>, }
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>, }
31
32#[derive(Deserialize, Debug)]
33struct Content {
34 parts: Vec<Part>,
35}
36
37#[derive(Deserialize, Debug)]
38struct Part {
39 text: String,
40}
41
42pub 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 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 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 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 if let Some(candidates) = data.candidates.as_ref() {
104 if let Some(first_candidate) = candidates.first() {
105 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
129pub 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 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 _ => {
191 error!("Gemini não retornou tradução para a query, usando original.");
192 Ok(text.to_string())
193 }
194 }
195}