The Gemini service has been updated to generate a character sheet rather than a single avatar image. This sheet includes the main character and separate assets for closed eyes and an open mouth. The `AvatarConfig` type and `RiggingEditor` component have been extended to handle these new expression assets (`textureClosedEye`, `textureOpenMouth`). A new `Sprite` component has been added to `Studio.tsx` to correctly render these specific regions from the generated character sheet. The UI has been updated to reflect the new generation process.
115 lines
4.7 KiB
TypeScript
115 lines
4.7 KiB
TypeScript
|
|
import React, { useState } from 'react';
|
|
import { generateAvatarImage } from '../services/geminiService';
|
|
import { analyzeAvatarImage } from '../services/visionService';
|
|
import LoadingSpinner from './LoadingSpinner';
|
|
import { Rect } from '../types';
|
|
|
|
interface AvatarCreatorProps {
|
|
onAvatarGenerated: (url: string, name: string, initialData?: { leftEye: Rect, rightEye: Rect, mouth: Rect, skinColor: string }) => void;
|
|
}
|
|
|
|
const AvatarCreator: React.FC<AvatarCreatorProps> = ({ onAvatarGenerated }) => {
|
|
const [prompt, setPrompt] = useState('');
|
|
const [name, setName] = useState('');
|
|
const [status, setStatus] = useState<'idle' | 'generating' | 'analyzing'>('idle');
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const handleGenerate = async () => {
|
|
if (!prompt || !name) return;
|
|
|
|
setStatus('generating');
|
|
setError(null);
|
|
|
|
try {
|
|
// 1. Generate Image (Now creates a character sheet)
|
|
const imageUrl = await generateAvatarImage(prompt);
|
|
|
|
// 2. Analyze Image for Landmarks (Initial guess)
|
|
// Note: Vision service will likely find the main face on the left, which is what we want for targets.
|
|
setStatus('analyzing');
|
|
const analysisData = await analyzeAvatarImage(imageUrl);
|
|
|
|
// 3. Pass to parent (to go to Rigging)
|
|
if (analysisData) {
|
|
onAvatarGenerated(imageUrl, name, analysisData);
|
|
} else {
|
|
onAvatarGenerated(imageUrl, name);
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
setError("Failed to generate avatar. Please try again.");
|
|
} finally {
|
|
setStatus('idle');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="max-w-2xl mx-auto bg-slate-800/50 backdrop-blur-lg border border-slate-700 p-8 rounded-2xl shadow-2xl">
|
|
<div className="text-center mb-8">
|
|
<h2 className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-purple-500 mb-2">
|
|
Design Your Avatar
|
|
</h2>
|
|
<p className="text-slate-400">
|
|
Describe your dream VTuber model. Gemini will generate a character sheet with expression assets.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-300 mb-2">Model Name</label>
|
|
<input
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="e.g., Neon Kitsune"
|
|
className="w-full bg-slate-900/50 border border-slate-600 rounded-xl px-4 py-3 text-white placeholder-slate-500 focus:ring-2 focus:ring-cyan-500 focus:border-transparent transition-all outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-300 mb-2">Description</label>
|
|
<textarea
|
|
value={prompt}
|
|
onChange={(e) => setPrompt(e.target.value)}
|
|
placeholder="e.g., A cyberpunk anime girl with neon blue hair, glowing headphones, wearing a futuristic jacket..."
|
|
className="w-full h-32 bg-slate-900/50 border border-slate-600 rounded-xl px-4 py-3 text-white placeholder-slate-500 focus:ring-2 focus:ring-cyan-500 focus:border-transparent transition-all outline-none resize-none"
|
|
/>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-red-200 text-sm">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
onClick={handleGenerate}
|
|
disabled={status !== 'idle' || !prompt || !name}
|
|
className={`w-full py-4 rounded-xl font-bold text-lg transition-all duration-200 ${
|
|
status !== 'idle' || !prompt || !name
|
|
? 'bg-slate-700 text-slate-500 cursor-not-allowed'
|
|
: 'bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 text-white shadow-lg shadow-cyan-500/25 transform hover:scale-[1.02]'
|
|
}`}
|
|
>
|
|
{status !== 'idle' ? (
|
|
<div className="flex items-center justify-center gap-3">
|
|
<LoadingSpinner />
|
|
<span>{status === 'generating' ? 'Dreaming up Sheet...' : 'Analyzing Features...'}</span>
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center justify-center gap-2">
|
|
<span>Generate Model</span>
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-8.707l-3-3a1 1 0 00-1.414 1.414L10.586 9H7a1 1 0 100 2h3.586l-1.293 1.293a1 1 0 101.414 1.414l3-3a1 1 0 000-1.414z" clipRule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AvatarCreator;
|