game_manager_lib\services/
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> {
43 info!("Iniciando tradução no Gemini (Modelo 2.5)...");
44
45 let url = format!("{}?key={}", GEMINI_API_URL, api_key);
46
47 let prompt = format!(
48 "Translate the following game description to Brazilian Portuguese (PT-BR). \
49 Maintain the tone (exciting, narrative). \
50 Keep technical gaming terms in English if they are commonly used by brazilian gamers (e.g., 'Roguelike', 'Metroidvania', 'Permadeath', 'Loot', 'Crafting'). \
51 Output ONLY the translated text, without preambles or markdown code blocks:\n\n{}",
52 text
53 );
54
55 let body = json!({
57 "contents": [{
58 "parts": [{
59 "text": prompt
60 }]
61 }],
62 "safetySettings": [
63 { "category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE" },
64 { "category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE" },
65 { "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE" },
66 { "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE" }
67 ]
68 });
69
70 let res = HTTP_CLIENT
71 .post(&url)
72 .json(&body)
73 .send()
74 .await
75 .map_err(|e| format!("Erro de rede Gemini: {}", e))?;
76
77 if !res.status().is_success() {
79 let status = res.status();
80 let error_body = res.text().await.unwrap_or_default();
81 error!("Erro API Gemini ({}): {}", status, error_body);
82 return Err(format!(
83 "A API retornou erro {}: Verifique sua chave ou cota.",
84 status
85 ));
86 }
87
88 let data: GeminiResponse = res
89 .json()
90 .await
91 .map_err(|e| format!("Erro ao ler JSON Gemini: {}", e))?;
92
93 if let Some(api_error) = data.error.as_ref() {
95 error!("Gemini API Error: {:?}", api_error);
96 return Err(format!("Gemini: {}", api_error.message));
97 }
98
99 if let Some(candidates) = data.candidates.as_ref() {
101 if let Some(first_candidate) = candidates.first() {
102 if let Some(reason) = &first_candidate.finish_reason {
104 if reason != "STOP" {
105 error!("Gemini bloqueou conteúdo. Motivo: {}", reason);
106 return Err(format!(
107 "Tradução bloqueada pelo filtro de segurança: {}",
108 reason
109 ));
110 }
111 }
112
113 if let Some(content) = &first_candidate.content {
114 if let Some(part) = content.parts.first() {
115 info!("Tradução concluída com sucesso.");
116 return Ok(part.text.trim().to_string());
117 }
118 }
119 }
120 }
121
122 error!("Resposta Gemini inesperada: {:?}", data);
123 Err("A IA não retornou nenhuma tradução válida.".to_string())
124}