Files
ReSekai/src/services/imageGen.ts
T

213 lines
5.6 KiB
TypeScript

// Image generation service using GeminiGen.ai (via backend proxy)
import type { CharacterAge, CharacterGender } from "../types";
const API_BASE = import.meta.env.VITE_API_URL || "http://localhost:3001";
const DEEPSEEK_API_URL = "https://api.deepseek.com/chat/completions";
const getDeepSeekKey = () => import.meta.env.VITE_DEEPSEEK_API_KEY || "";
interface GenerateAvatarOptions {
description: string;
name?: string;
age?: CharacterAge;
gender?: CharacterGender;
customPrompt?: string; // Если задан - используется напрямую
isNsfw?: boolean;
}
const AGE_PROMPTS: Record<CharacterAge, string> = {
child: "young child, cute, innocent",
teenager: "teenager, young, youthful",
adult: "adult, mature",
elderly: "elderly, old, wise, wrinkled",
};
const GENDER_PROMPTS: Record<CharacterGender, string> = {
male: "male, man, boy",
female: "female, woman, girl",
};
/**
* Translates Russian text to English using DeepSeek API
*/
async function translateToEnglish(text: string): Promise<string> {
const apiKey = getDeepSeekKey();
if (!apiKey) {
console.warn("No DeepSeek API key for translation");
return text;
}
try {
const response = await fetch(DEEPSEEK_API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: "deepseek-chat",
messages: [
{
role: "system",
content:
"Translate to English for image generation. Output ONLY the translation, nothing else. Keep character names as-is. Be concise.",
},
{
role: "user",
content: text,
},
],
temperature: 0.1,
max_tokens: 150,
}),
});
if (!response.ok) {
console.error("Translation failed:", response.status);
return text;
}
const data = await response.json();
const translated = data.choices?.[0]?.message?.content?.trim();
console.log("Translated prompt:", translated);
return translated || text;
} catch (error) {
console.error("Translation error:", error);
return text;
}
}
/**
* Generates an image from a prompt (portrait format)
* Returns image URL
*/
async function generateImageFromPrompt(prompt: string): Promise<string> {
console.log("Generating image with prompt:", prompt);
const response = await fetch(`${API_BASE}/api/generate-image`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify({ prompt }),
});
if (!response.ok) {
const error = await response.text();
console.error("Grok API error:", error);
throw new Error(`Failed to generate image: ${response.status}`);
}
const data = await response.json();
console.log("GeminiGen response:", data);
if (data.url) {
return data.url;
}
if (data.pending && data.uuid) {
return await pollForResult(data.uuid);
}
throw new Error("Unexpected response from image generation API");
}
/**
* Generates an avatar image using Grok via GeminiGen.ai
* If customPrompt is provided, uses it directly
* Otherwise builds prompt from description, age, gender
* Returns image URL
*/
export async function generateAvatarUrl(
options: GenerateAvatarOptions,
): Promise<string> {
const {
description,
name,
age = "adult",
gender = "female",
customPrompt,
isNsfw,
} = options;
// If custom prompt provided - use it directly
if (customPrompt && customPrompt.trim()) {
return generateImageFromPrompt(customPrompt.trim());
}
// Build automatic prompt
const firstSentence = description
.split(/[.!?。]/)[0]
.replace(/\{user\}/gi, name || "character")
.trim()
.slice(0, 200);
const textToTranslate = name ? `${name} - ${firstSentence}` : firstSentence;
const englishDesc = await translateToEnglish(textToTranslate);
const ageDesc = AGE_PROMPTS[age] || AGE_PROMPTS.adult;
const genderDesc = GENDER_PROMPTS[gender] || GENDER_PROMPTS.female;
const nsfwTag = isNsfw ? "nsfw, explicit, " : "";
const prompt = `${nsfwTag}anime illustration, ${englishDesc}, ${genderDesc}, ${ageDesc}, full body from head to toe, entire body visible, standing pose, feet visible on ground, wide shot, masterpiece, best quality, highly detailed, vibrant colors`;
return generateImageFromPrompt(prompt);
}
/**
* Poll for image generation result
*/
async function pollForResult(uuid: string): Promise<string> {
const maxAttempts = 60; // 2 minutes max
const pollInterval = 2000; // 2 seconds
for (let i = 0; i < maxAttempts; i++) {
await new Promise((resolve) => setTimeout(resolve, pollInterval));
const response = await fetch(
`${API_BASE}/api/generate-image/status/${uuid}`,
{
credentials: "include",
},
);
if (!response.ok) {
throw new Error("Failed to check generation status");
}
const data = await response.json();
console.log("Generation status:", data);
if (data.done && data.url) {
return data.url;
}
if (data.error) {
throw new Error(data.error);
}
// Still pending, continue polling
}
throw new Error("Image generation timed out");
}
/**
* Pre-fetches an image to check if it loads correctly
*/
export async function validateImageUrl(url: string): Promise<boolean> {
if (url.startsWith("data:")) {
return true; // Base64 is always valid
}
return new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(true);
img.onerror = () => resolve(false);
setTimeout(() => resolve(false), 60000);
img.src = url;
});
}