213 lines
5.6 KiB
TypeScript
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;
|
|
});
|
|
}
|