piclets / src /lib /components /MonsterGenerator /MonsterGenerator.svelte
Fraser's picture
CLOSER
334ad08
raw
history blame
8.96 kB
<script lang="ts">
import type { MonsterGeneratorProps, MonsterWorkflowState, CaptionType, CaptionLength } from '$lib/types';
import UploadStep from './UploadStep.svelte';
import WorkflowProgress from './WorkflowProgress.svelte';
import MonsterResult from './MonsterResult.svelte';
interface Props extends MonsterGeneratorProps {}
let { joyCaptionClient, rwkvClient, fluxClient }: Props = $props();
let state: MonsterWorkflowState = $state({
currentStep: 'upload',
userImage: null,
imageCaption: null,
monsterConcept: null,
imagePrompt: null,
monsterImage: null,
error: null,
isProcessing: false
});
// Prompt templates
const MONSTER_CONCEPT_PROMPT = (caption: string) => `User: Based on this description of a person/photo: "${caption}"
Create a unique monster concept inspired by the key visual elements. Include:
- Physical appearance and distinguishing features
- Special abilities or powers
- Personality traits
- Habitat or origin
Be creative and detailed. The monster should be fantastical but somehow connected to the original image's essence.
Assistant:`;
const IMAGE_GENERATION_PROMPT = (concept: string) => `User: Convert this monster concept into a detailed image generation prompt:
"${concept}"
Create a vivid, artistic prompt focusing on visual details, art style, lighting, and atmosphere. Format it as a single paragraph optimized for image generation.
Assistant:`;
async function handleImageSelected(file: File) {
if (!joyCaptionClient || !rwkvClient || !fluxClient) {
state.error = "Services not connected. Please wait...";
return;
}
state.userImage = file;
state.error = null;
startWorkflow();
}
async function startWorkflow() {
state.isProcessing = true;
try {
// Step 1: Caption the image
await captionImage();
await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for state update
// Step 2: Generate monster concept
await generateMonsterConcept();
await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for state update
// Step 3: Generate image prompt
await generateImagePrompt();
await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for state update
// Step 4: Generate monster image
await generateMonsterImage();
state.currentStep = 'complete';
} catch (err) {
console.error('Workflow error:', err);
state.error = err instanceof Error ? err.message : 'An unknown error occurred';
} finally {
state.isProcessing = false;
}
}
async function captionImage() {
state.currentStep = 'captioning';
if (!joyCaptionClient || !state.userImage) {
throw new Error('Caption service not available or no image provided');
}
const output = await joyCaptionClient.predict("/stream_chat", [
state.userImage,
"Descriptive" as CaptionType,
"very long" as CaptionLength,
[], // extra_options
"", // name_input
"" // custom_prompt
]);
const [prompt, caption] = output.data;
state.imageCaption = caption;
console.log('Caption generated:', caption);
}
async function generateMonsterConcept() {
state.currentStep = 'conceptualizing';
if (!rwkvClient || !state.imageCaption) {
throw new Error('Text generation service not available or no caption');
}
const conceptPrompt = MONSTER_CONCEPT_PROMPT(state.imageCaption);
console.log('Generating monster concept with prompt:', conceptPrompt);
const output = await rwkvClient.predict(0, [
conceptPrompt,
300, // maxTokens
1.2, // temperature (more creative)
0.8, // topP
0.1, // presencePenalty
0.1 // countPenalty
]);
console.log('RWKV output:', output);
state.monsterConcept = output.data[0];
console.log('Monster concept generated:', state.monsterConcept);
if (!state.monsterConcept || state.monsterConcept.trim() === '') {
throw new Error('Failed to generate monster concept - received empty response');
}
}
async function generateImagePrompt() {
state.currentStep = 'promptCrafting';
if (!rwkvClient || !state.monsterConcept) {
throw new Error('Text generation service not available or no concept');
}
const promptGenerationPrompt = IMAGE_GENERATION_PROMPT(state.monsterConcept);
console.log('Generating image prompt from concept');
const output = await rwkvClient.predict(0, [
promptGenerationPrompt,
150, // maxTokens
0.9, // temperature
0.7, // topP
0.1, // presencePenalty
0.1 // countPenalty
]);
console.log('Image prompt output:', output);
state.imagePrompt = output.data[0];
console.log('Image prompt generated:', state.imagePrompt);
if (!state.imagePrompt || state.imagePrompt.trim() === '') {
throw new Error('Failed to generate image prompt - received empty response');
}
}
async function generateMonsterImage() {
state.currentStep = 'generating';
if (!fluxClient || !state.imagePrompt) {
throw new Error('Image generation service not available or no prompt');
}
const output = await fluxClient.predict("/infer", [
state.imagePrompt,
0, // seed
true, // randomizeSeed
1024, // width
1024, // height
4 // steps
]);
const [image, usedSeed] = output.data;
let url: string | undefined;
if (typeof image === "string") url = image;
else if (image && image.url) url = image.url;
else if (image && image.path) url = image.path;
if (url) {
state.monsterImage = {
imageUrl: url,
seed: usedSeed,
prompt: state.imagePrompt
};
} else {
throw new Error('Failed to generate monster image');
}
}
function reset() {
state = {
currentStep: 'upload',
userImage: null,
imageCaption: null,
monsterConcept: null,
imagePrompt: null,
monsterImage: null,
error: null,
isProcessing: false
};
}
</script>
<div class="monster-generator">
{#if state.currentStep !== 'upload'}
<WorkflowProgress currentStep={state.currentStep} error={state.error} />
{/if}
{#if state.currentStep === 'upload'}
<UploadStep
onImageSelected={handleImageSelected}
isProcessing={state.isProcessing}
/>
{:else if state.currentStep === 'complete'}
<MonsterResult state={state} onReset={reset} />
{:else}
<div class="processing-container">
<div class="spinner"></div>
<p class="processing-text">
{#if state.currentStep === 'captioning'}
Analyzing your image...
{:else if state.currentStep === 'conceptualizing'}
Creating monster concept...
{:else if state.currentStep === 'promptCrafting'}
Crafting generation prompt...
{:else if state.currentStep === 'generating'}
Generating your monster...
{/if}
</p>
{#if state.imageCaption && state.currentStep !== 'captioning'}
<div class="intermediate-result">
<h4>Image Description:</h4>
<p>{state.imageCaption}</p>
</div>
{/if}
{#if state.monsterConcept && state.currentStep !== 'captioning' && state.currentStep !== 'conceptualizing'}
<div class="intermediate-result">
<h4>Monster Concept:</h4>
<p>{state.monsterConcept}</p>
</div>
{/if}
{#if state.imagePrompt && state.currentStep === 'generating'}
<div class="intermediate-result">
<h4>Generation Prompt:</h4>
<p>{state.imagePrompt}</p>
</div>
{/if}
</div>
{/if}
</div>
<style>
.monster-generator {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.processing-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 3rem 1rem;
}
.spinner {
width: 60px;
height: 60px;
border: 3px solid #f3f3f3;
border-top: 3px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 2rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.processing-text {
font-size: 1.2rem;
color: #333;
margin-bottom: 2rem;
}
.intermediate-result {
width: 100%;
max-width: 700px;
background: #f8f9fa;
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 1.5rem;
text-align: left;
}
.intermediate-result h4 {
margin: 0 0 0.5rem 0;
color: #495057;
}
.intermediate-result p {
margin: 0;
line-height: 1.6;
color: #333;
}
</style>