feat: NPC system improvements - custom prompt, NSFW, full body generation
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
// 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;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user