testgemini/services/geminiService.ts

675 lines
23 KiB
TypeScript
Raw Normal View History

2026-01-26 00:57:06 +08:00
import { GoogleGenAI } from "@google/genai";
const apiKey = process.env.API_KEY;
const baseUrl = process.env.BASE_URL;
const ai = new GoogleGenAI({
apiKey: apiKey || '',
...(baseUrl && { httpOptions: { baseUrl } })
});
/**
* Helper to extract MIME type and base64 data from a data URL.
*/
const extractBase64Data = (base64String: string): { mimeType: string; data: string } => {
// Regex to capture mime type and data
const matches = base64String.match(/^data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+);base64,(.+)$/);
if (matches && matches.length === 3) {
return {
mimeType: matches[1],
data: matches[2]
};
}
// Fallback for strings that might just be the base64 data or simple split
const split = base64String.split(',');
return {
mimeType: 'image/png', // Default fallback
data: split.length > 1 ? split[1] : split[0]
};
};
/**
* Determines the best aspect ratio for the requested grid layout.
*
* 2x2 -> 1:1 (Square)
* 2x3 -> 16:9 (Wide) - 4:3 is okay too, but 16:9 gives more width for separation
* 2x4 -> 16:9 (Wide)
* 3x3 -> 1:1 (Square)
*/
const getBestAspectRatio = (rows: number, cols: number): string => {
const ratio = cols / rows;
if (ratio >= 1.8) return "16:9"; // e.g. 2x4 = 2.0
if (ratio >= 1.3) return "4:3"; // e.g. 2x3 = 1.5 (16:9 is also fine here, but 4:3 works)
if (ratio <= 0.7) return "3:4";
if (ratio <= 0.5) return "9:16";
return "1:1"; // Default for 2x2, 3x3
};
/**
* Generate a sprite sheet using the original image and the action prompt.
* If no image is provided, generates from text description only.
*/
export const generateSpriteSheet = async (
originalBase64: string | null,
action: string,
rows: number = 2,
cols: number = 3,
characterDescription?: string
): Promise<string> => {
if (!apiKey) throw new Error("API Key not found");
const totalFrames = rows * cols;
const aspectRatio = getBestAspectRatio(rows, cols);
// Build prompt based on whether we have a reference image
const hasImage = originalBase64 !== null;
const prompt = hasImage
? `
Generate a clean 2D Sprite Sheet.
INPUT ACTION: ${action}
LAYOUT CONFIGURATION:
- GRID: ${rows} rows by ${cols} columns.
- TOTAL SPRITES: ${totalFrames}
- STYLE: Flat illustration, white background.
CRITICAL RULES:
1. **STRICT GRID**: You MUST draw exactly ${rows} rows and ${cols} columns.
2. **NO NUMBERS**: Do NOT include any numbers, text, arrows, or guide lines.
3. **ISOLATION**: Draw each character SMALLER than the grid cell. Leave clear white space around every character.
4. **NO OVERLAP**: Characters must NOT touch the imaginary grid lines.
5. **CONSISTENCY**: Keep the character size and proportions identical in every frame.
Output ONLY the image on a solid white background.
`
: `
Generate a clean 2D Sprite Sheet from scratch.
CHARACTER DESCRIPTION: ${characterDescription || '一个可爱的卡通角色'}
INPUT ACTION: ${action}
LAYOUT CONFIGURATION:
- GRID: ${rows} rows by ${cols} columns.
- TOTAL SPRITES: ${totalFrames}
- STYLE: Flat illustration, white background, 2D cartoon style.
CRITICAL RULES:
1. **STRICT GRID**: You MUST draw exactly ${rows} rows and ${cols} columns.
2. **NO NUMBERS**: Do NOT include any numbers, text, arrows, or guide lines.
3. **ISOLATION**: Draw each character SMALLER than the grid cell. Leave clear white space around every character.
4. **NO OVERLAP**: Characters must NOT touch the imaginary grid lines.
5. **CONSISTENCY**: Keep the character size and proportions identical in every frame.
6. **ANIMATION FLOW**: Each frame should show a smooth progression of the action.
Output ONLY the image on a solid white background.
`;
// Build content parts
const parts: any[] = [];
if (hasImage) {
const { mimeType, data } = extractBase64Data(originalBase64);
parts.push({
inlineData: {
mimeType: mimeType,
data: data
}
});
}
parts.push({ text: prompt });
try {
const response = await ai.models.generateContent({
model: 'gemini-2.5-flash-image',
contents: {
parts: parts,
},
config: {
temperature: 0.2,
imageConfig: {
aspectRatio: aspectRatio
}
}
});
// Extract the image from the response
for (const part of response.candidates?.[0]?.content?.parts || []) {
if (part.inlineData) {
return `data:image/png;base64,${part.inlineData.data}`;
}
}
const textPart = response.candidates?.[0]?.content?.parts?.find(p => p.text);
if (textPart && textPart.text) {
console.warn("Model returned text instead of image:", textPart.text);
throw new Error("生成失败:模型拒绝了请求 (可能是安全策略)");
}
throw new Error("生成失败:未返回图像数据");
} catch (error: any) {
console.error("Generation failed", error);
throw new Error(error.message || "生成失败,请重试");
}
};
/**
* Generate a character portrait/illustration from text description.
*/
export const generateCharacterPortrait = async (
characterDescription: string,
style: string = '2D卡通风格',
aspectRatio: '1:1' | '3:4' | '9:16' = '3:4'
): Promise<string> => {
if (!apiKey) throw new Error("API Key not found");
const prompt = `
Generate a single character portrait illustration.
CHARACTER DESCRIPTION: ${characterDescription}
ART STYLE: ${style}
REQUIREMENTS:
1. Full body or upper body character portrait
2. Clean, professional illustration style
3. Transparent or solid color background
4. High quality, detailed artwork
5. The character should be centered in the image
6. No text, watermarks, or extra elements
Output a single, high-quality character illustration.
`;
try {
const response = await ai.models.generateContent({
model: 'gemini-2.5-flash-image',
contents: {
parts: [{ text: prompt }],
},
config: {
temperature: 0.7,
imageConfig: {
aspectRatio: aspectRatio
}
}
});
for (const part of response.candidates?.[0]?.content?.parts || []) {
if (part.inlineData) {
return `data:image/png;base64,${part.inlineData.data}`;
}
}
const textPart = response.candidates?.[0]?.content?.parts?.find(p => p.text);
if (textPart && textPart.text) {
console.warn("Model returned text instead of image:", textPart.text);
throw new Error("生成失败:模型拒绝了请求 (可能是安全策略)");
}
throw new Error("生成失败:未返回图像数据");
} catch (error: any) {
console.error("Portrait generation failed", error);
throw new Error(error.message || "立绘生成失败,请重试");
}
};
/**
* Generate a 4-direction sprite sheet (4 rows x 4 cols).
* Each row represents a direction: Down, Left, Right, Up
* Each row has 4 animation frames for walking/running.
*/
export const generateDirectionalSpriteSheet = async (
originalBase64: string | null,
action: string = '行走',
characterDescription?: string
): Promise<string> => {
if (!apiKey) throw new Error("API Key not found");
const hasImage = originalBase64 !== null;
const prompt = hasImage
? `
Generate a 4-direction character sprite sheet for game development.
INPUT ACTION: ${action}
LAYOUT CONFIGURATION:
- GRID: 4 rows by 4 columns (16 sprites total)
- Row 1: Downward walking animation - Character facing down the screen, 4 consecutive frames
- Row 2: Leftward walking animation - Character facing left of the screen, 4 consecutive frames
- Row 3: Rightward walking animation - Character facing right of the screen, 4 consecutive frames
- Row 4: Upward walking animation - Character facing up the screen, 4 consecutive frames
- STYLE: Flat illustration, white background, pixel art or 2D game style.
CRITICAL RULES:
1. **STRICT GRID**: You MUST draw exactly 4 rows and 4 columns.
2. **DIRECTION ORDER**: Down, Left, Right, Up (top to bottom).
3. **NO NUMBERS/TEXT**: Do NOT include any numbers, text, arrows, or guide lines.
4. **ISOLATION**: Draw each character SMALLER than the grid cell. Leave clear white space around every character.
5. **NO OVERLAP**: Characters must NOT touch the imaginary grid lines.
6. **CONSISTENCY**: Keep the character size and proportions identical in every frame.
7. **ANIMATION FLOW**: Each row shows smooth walking/running animation in that direction.
Output ONLY the image on a solid white background.
`
: `
Generate a 4-direction character sprite sheet for game development from scratch.
CHARACTER DESCRIPTION: ${characterDescription || '一个可爱的卡通角色'}
INPUT ACTION: ${action}
LAYOUT CONFIGURATION:
- GRID: 4 rows by 4 columns (16 sprites total)
- Row 1: Downward walking animation - Character facing down the screen, 4 consecutive frames
- Row 2: Leftward walking animation - Character facing left of the screen, 4 consecutive frames
- Row 3: Rightward walking animation - Character facing right of the screen, 4 consecutive frames
- Row 4: Upward walking animation - Character facing up the screen, 4 consecutive frames
- STYLE: Flat illustration, white background, 2D cartoon/game style.
CRITICAL RULES:
1. **STRICT GRID**: You MUST draw exactly 4 rows and 4 columns.
2. **DIRECTION ORDER**: Down, Left, Right, Up (top to bottom).
3. **NO NUMBERS/TEXT**: Do NOT include any numbers, text, arrows, or guide lines.
4. **ISOLATION**: Draw each character SMALLER than the grid cell. Leave clear white space around every character.
5. **NO OVERLAP**: Characters must NOT touch the imaginary grid lines.
6. **CONSISTENCY**: Keep the character size and proportions identical in every frame.
7. **ANIMATION FLOW**: Each row shows smooth walking/running animation in that direction.
Output ONLY the image on a solid white background.
`;
const parts: any[] = [];
if (hasImage) {
const { mimeType, data } = extractBase64Data(originalBase64);
parts.push({
inlineData: {
mimeType: mimeType,
data: data
}
});
}
parts.push({ text: prompt });
try {
const response = await ai.models.generateContent({
model: 'gemini-2.5-flash-image',
contents: {
parts: parts,
},
config: {
temperature: 0.2,
imageConfig: {
aspectRatio: "1:1"
}
}
});
for (const part of response.candidates?.[0]?.content?.parts || []) {
if (part.inlineData) {
return `data:image/png;base64,${part.inlineData.data}`;
}
}
const textPart = response.candidates?.[0]?.content?.parts?.find(p => p.text);
if (textPart && textPart.text) {
console.warn("Model returned text instead of image:", textPart.text);
throw new Error("生成失败:模型拒绝了请求 (可能是安全策略)");
}
throw new Error("生成失败:未返回图像数据");
} catch (error: any) {
console.error("Directional sprite sheet generation failed", error);
throw new Error(error.message || "序列帧生成失败,请重试");
}
};
/**
* Map types for game map generation
*/
export type MapType = 'rpg' | 'tilemap' | 'platformer' | 'strategy' | 'world';
/**
* Generate game maps of various types
*/
export const generateGameMap = async (
mapType: MapType,
description: string,
style: string = '像素风格'
): Promise<string> => {
if (!apiKey) throw new Error("API Key not found");
const mapPrompts: Record<MapType, string> = {
rpg: `
Generate a top-down RPG game map scene.
SCENE DESCRIPTION: ${description}
ART STYLE: ${style}
REQUIREMENTS:
- Top-down/bird's eye view perspective
- Include terrain features (grass, paths, water, trees, buildings)
- Game-ready art style with clear boundaries
- Rich details but not cluttered
- Suitable for character movement
- No UI elements, text, or markers
Output a single cohesive map image.
`,
tilemap: `
Generate a tilemap sprite sheet for game development.
THEME: ${description}
ART STYLE: ${style}
LAYOUT: 8x8 grid of tiles (64 tiles total)
TILE CATEGORIES TO INCLUDE:
- Ground tiles (grass, dirt, sand, stone)
- Water tiles (with edges/transitions)
- Path/road tiles (straight, corners, intersections)
- Decoration tiles (flowers, rocks, bushes)
- Building tiles (walls, roofs, doors, windows)
- Tree/forest tiles
- Transition tiles between different terrains
REQUIREMENTS:
- Each tile must be clearly separated
- Tiles should be seamlessly tileable
- Consistent art style across all tiles
- White or transparent gaps between tiles
- No text, numbers, or labels
Output ONLY the tilemap sprite sheet.
`,
platformer: `
Generate a side-scrolling platformer game level background.
SCENE DESCRIPTION: ${description}
ART STYLE: ${style}
REQUIREMENTS:
- Side view perspective
- Multiple parallax layers suggested (foreground, midground, background)
- Include platforms, obstacles, and environmental elements
- Clear ground/floor area for character to walk on
- Atmospheric and immersive
- Game-ready, not too detailed to distract from gameplay
- No characters, UI, or text
Output a wide panoramic scene image.
`,
strategy: `
Generate a grid-based strategy/tactics game battle map.
SCENE DESCRIPTION: ${description}
ART STYLE: ${style}
REQUIREMENTS:
- Clear grid pattern (hexagonal or square)
- Various terrain types with different tactical value
- Include obstacles, cover positions, high ground
- Bird's eye view perspective
- Balanced layout suitable for tactical combat
- Clear visual distinction between terrain types
- No units, characters, or UI elements
Output a tactical battle map image.
`,
world: `
Generate a fantasy world map / continent overview.
DESCRIPTION: ${description}
ART STYLE: ${style}
REQUIREMENTS:
- Cartographic/illustrated map style
- Include continents, islands, oceans
- Show mountain ranges, forests, deserts, rivers
- Mark major cities/locations with simple icons (no text)
- Compass rose or decorative border optional
- Fantasy/medieval cartography aesthetic
- Parchment or clean background
- No modern elements
Output a complete world map image.
`
};
const prompt = mapPrompts[mapType];
// Choose aspect ratio based on map type
const aspectRatios: Record<MapType, string> = {
rpg: '1:1',
tilemap: '1:1',
platformer: '16:9',
strategy: '1:1',
world: '4:3'
};
try {
const response = await ai.models.generateContent({
model: 'gemini-2.5-flash-image',
contents: {
parts: [{ text: prompt }],
},
config: {
temperature: 0.7,
imageConfig: {
aspectRatio: aspectRatios[mapType]
}
}
});
for (const part of response.candidates?.[0]?.content?.parts || []) {
if (part.inlineData) {
return `data:image/png;base64,${part.inlineData.data}`;
}
}
const textPart = response.candidates?.[0]?.content?.parts?.find(p => p.text);
if (textPart && textPart.text) {
console.warn("Model returned text instead of image:", textPart.text);
throw new Error("生成失败:模型拒绝了请求 (可能是安全策略)");
}
throw new Error("生成失败:未返回图像数据");
} catch (error: any) {
console.error("Map generation failed", error);
throw new Error(error.message || "地图生成失败,请重试");
}
};
/**
* Item types for game item generation
*/
export type ItemType = 'weapon' | 'armor' | 'potion' | 'material' | 'treasure' | 'accessory';
/**
* Generate game items (weapons, armor, potions, etc.)
*/
export const generateGameItem = async (
itemType: ItemType,
description: string,
style: string = '像素风格',
quantity: 'single' | 'set' = 'single'
): Promise<string> => {
if (!apiKey) throw new Error("API Key not found");
const itemPrompts: Record<ItemType, string> = {
weapon: `
Generate game weapon sprite${quantity === 'set' ? 's (6 variations in a 2x3 grid)' : ''}.
WEAPON DESCRIPTION: ${description}
ART STYLE: ${style}
${quantity === 'set' ? `
LAYOUT: 2 rows x 3 columns grid (6 weapons total)
IMPORTANT: All 6 weapons must be the SAME TYPE as described above.
Generate 6 variations/upgrades of "${description}" - different colors, materials, enchantments, or quality levels.
Example: If description is "fire sword", generate 6 different fire swords with varying flame effects.
` : ''}
REQUIREMENTS:
- Clean isolated weapon on transparent/white background
- Game-ready icon style
- Clear silhouette and details
- Consistent lighting (top-left light source)
- No hands, characters, or extra elements
- Professional game asset quality
Output ${quantity === 'set' ? 'a grid of 6 similar weapons with variations' : 'a single weapon'} image.
`,
armor: `
Generate game armor/equipment sprite${quantity === 'set' ? 's (6 variations in a 2x3 grid)' : ''}.
ARMOR DESCRIPTION: ${description}
ART STYLE: ${style}
${quantity === 'set' ? `
LAYOUT: 2 rows x 3 columns grid (6 pieces total)
IMPORTANT: All 6 pieces must be the SAME TYPE as described above.
Generate 6 variations/upgrades of "${description}" - different colors, materials, or quality levels.
Example: If description is "knight helmet", generate 6 different knight helmets with varying designs.
` : ''}
REQUIREMENTS:
- Clean isolated equipment on transparent/white background
- Game-ready icon style
- Clear details and textures
- Consistent lighting
- No characters wearing them
- Professional game asset quality
Output ${quantity === 'set' ? 'a grid of 6 similar armor pieces with variations' : 'a single armor piece'} image.
`,
potion: `
Generate game potion/consumable sprite${quantity === 'set' ? 's (6 variations in a 2x3 grid)' : ''}.
ITEM DESCRIPTION: ${description}
ART STYLE: ${style}
${quantity === 'set' ? `
LAYOUT: 2 rows x 3 columns grid (6 items total)
IMPORTANT: All 6 items must be the SAME TYPE as described above.
Generate 6 variations of "${description}" - different sizes, fill levels, or potency indicators.
Example: If description is "health potion", generate 6 health potions (small/medium/large, different fill levels).
` : ''}
REQUIREMENTS:
- Clean isolated items on transparent/white background
- Game-ready icon style
- Glowing/magical effects where appropriate
- Clear bottle/container shapes
- Professional game asset quality
Output ${quantity === 'set' ? 'a grid of 6 similar consumables with variations' : 'a single consumable'} image.
`,
material: `
Generate game crafting material sprite${quantity === 'set' ? 's (6 variations in a 2x3 grid)' : ''}.
MATERIAL DESCRIPTION: ${description}
ART STYLE: ${style}
${quantity === 'set' ? `
LAYOUT: 2 rows x 3 columns grid (6 materials total)
IMPORTANT: All 6 materials must be the SAME TYPE as described above.
Generate 6 variations of "${description}" - different quantities, quality levels, or processing stages.
Example: If description is "iron ore", generate 6 iron ores (raw/refined, different sizes, different purities).
` : ''}
REQUIREMENTS:
- Clean isolated materials on transparent/white background
- Game-ready icon style
- Natural textures and details
- Clear identification of material type
- Professional game asset quality
Output ${quantity === 'set' ? 'a grid of 6 similar materials with variations' : 'a single material'} image.
`,
treasure: `
Generate game treasure/container sprite${quantity === 'set' ? 's (6 variations in a 2x3 grid)' : ''}.
TREASURE DESCRIPTION: ${description}
ART STYLE: ${style}
${quantity === 'set' ? `
LAYOUT: 2 rows x 3 columns grid (6 items total)
IMPORTANT: All 6 items must be the SAME TYPE as described above.
Generate 6 variations of "${description}" - different states, sizes, or value levels.
Example: If description is "treasure chest", generate 6 treasure chests (closed/open, wooden/golden, small/large).
` : ''}
REQUIREMENTS:
- Clean isolated items on transparent/white background
- Game-ready icon style
- Rich, valuable appearance
- Gold/metallic sheen where appropriate
- Professional game asset quality
Output ${quantity === 'set' ? 'a grid of 6 similar treasures with variations' : 'a single treasure'} image.
`,
accessory: `
Generate game accessory sprite${quantity === 'set' ? 's (6 variations in a 2x3 grid)' : ''}.
ACCESSORY DESCRIPTION: ${description}
ART STYLE: ${style}
${quantity === 'set' ? `
LAYOUT: 2 rows x 3 columns grid (6 items total)
IMPORTANT: All 6 items must be the SAME TYPE as described above.
Generate 6 variations of "${description}" - different colors, gem types, or enchantment effects.
Example: If description is "magic ring", generate 6 magic rings with different gem colors and effects.
` : ''}
REQUIREMENTS:
- Clean isolated accessories on transparent/white background
- Game-ready icon style
- Magical glow effects where appropriate
- Fine details on jewelry
- Professional game asset quality
Output ${quantity === 'set' ? 'a grid of 6 similar accessories with variations' : 'a single accessory'} image.
`
};
const prompt = itemPrompts[itemType];
try {
const response = await ai.models.generateContent({
model: 'gemini-2.5-flash-image',
contents: {
parts: [{ text: prompt }],
},
config: {
temperature: 0.7,
imageConfig: {
aspectRatio: quantity === 'set' ? '4:3' : '1:1'
}
}
});
for (const part of response.candidates?.[0]?.content?.parts || []) {
if (part.inlineData) {
return `data:image/png;base64,${part.inlineData.data}`;
}
}
const textPart = response.candidates?.[0]?.content?.parts?.find(p => p.text);
if (textPart && textPart.text) {
console.warn("Model returned text instead of image:", textPart.text);
throw new Error("生成失败:模型拒绝了请求 (可能是安全策略)");
}
throw new Error("生成失败:未返回图像数据");
} catch (error: any) {
console.error("Item generation failed", error);
throw new Error(error.message || "道具生成失败,请重试");
}
};