File size: 33,275 Bytes
415a17d 55b1a24 565e57b 415a17d 55b1a24 edc4c90 213c234 8db63ba fd1786e 565e57b a8636b2 82f6d8e f0b54f8 82f6d8e 991f849 48e6066 d009748 de7bb17 82f6d8e 24561f7 48e6066 4164789 48e6066 4164789 48e6066 8d5e74d 48e6066 8d5e74d 0268d3c 48e6066 4164789 415a17d 55b1a24 415a17d 55b1a24 415a17d 565e57b 415a17d d081d7a e223b2b 415a17d e223b2b 415a17d 27fe49f 213c234 4164789 213c234 4164789 213c234 55b1a24 213c234 4164789 213c234 4164789 213c234 415a17d 24561f7 4164789 415a17d d081d7a 4164789 213c234 415a17d d081d7a 4164789 d081d7a 4164789 d081d7a 415a17d 4164789 9a3c041 415a17d 9a3c041 415a17d 4164789 9a3c041 0268d3c 782c122 4164789 9a3c041 c703ea3 4164789 9a3c041 0268d3c 782c122 4164789 9a3c041 415a17d 9a3c041 c703ea3 9a3c041 4164789 9a3c041 991f849 d081d7a 415a17d 9a3c041 e6edab1 4164789 e6edab1 4164789 e6edab1 4164789 e6edab1 4164789 e6edab1 415a17d 4164789 415a17d e6edab1 415a17d 4164789 565e57b 4164789 415a17d 565e57b e6edab1 ce1f183 565e57b ce1f183 565e57b 9a3c041 e6edab1 415a17d 782c122 4164789 565e57b ce1f183 782c122 ce1f183 782c122 ce1f183 782c122 ce1f183 5c6b653 ce1f183 fba600d ce1f183 782c122 ce1f183 782c122 ce1f183 782c122 c703ea3 ce1f183 c703ea3 9a3c041 ce1f183 9a3c041 6a7fe5b ce1f183 55f5c6c ce1f183 782c122 4164789 ce1f183 01393b5 ce1f183 83f8807 01393b5 ce1f183 782c122 5c6b653 55f5c6c 5c6b653 782c122 415a17d 782c122 4164789 415a17d 48e6066 415a17d 01393b5 415a17d 55f5c6c 782c122 0268d3c 782c122 4164789 782c122 4164789 782c122 4164789 782c122 415a17d 782c122 4164789 415a17d 4164789 782c122 415a17d 782c122 2531fcd 4164789 2531fcd 1a441d0 2531fcd 1a441d0 2531fcd e6edab1 d5e2d02 e6edab1 edc4c90 d9c705e edc4c90 4164789 d9c705e 4164789 d9c705e edc4c90 d9c705e edc4c90 d9c705e 4164789 d9c705e 4164789 d9c705e e6edab1 415a17d c703ea3 4164789 9a3c041 c703ea3 ac7c05c 9a3c041 d9c705e 01393b5 c703ea3 9a3c041 c703ea3 83f8807 c703ea3 782c122 c703ea3 9a3c041 01393b5 c703ea3 9a3c041 c703ea3 9a3c041 01393b5 c703ea3 83f8807 9a3c041 01393b5 ce1f183 9a3c041 01393b5 9a3c041 83f8807 9a3c041 01393b5 83f8807 9a3c041 c703ea3 9a3c041 c703ea3 9a3c041 c703ea3 9a3c041 d9c705e 9a3c041 d9c705e c703ea3 4164789 600cde8 544b046 4164789 544b046 55b1a24 4164789 55b1a24 600cde8 544b046 55b1a24 544b046 600cde8 c703ea3 b78dca2 fba600d 01393b5 b78dca2 82f6d8e fba600d 01393b5 d009748 23a36f6 c703ea3 a8636b2 600cde8 55b1a24 4164789 544b046 991f849 600cde8 415a17d 4164789 415a17d 55b1a24 415a17d 55b1a24 415a17d 01393b5 415a17d 55b1a24 415a17d 4164789 415a17d 4164789 415a17d d081d7a 4164789 d081d7a 415a17d 4164789 415a17d 4164789 782c122 4164789 081e575 4164789 9a3c041 4164789 782c122 4164789 ab8a147 415a17d 55b1a24 415a17d 4164789 415a17d |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 |
<script lang="ts">
import type { PicletGeneratorProps, PicletWorkflowState, CaptionType, CaptionLength, PicletStats } from '$lib/types';
import type { PicletInstance, DiscoveryStatus } from '$lib/db/schema';
import UploadStep from './UploadStep.svelte';
import WorkflowProgress from './WorkflowProgress.svelte';
import PicletResult from './PicletResult.svelte';
import { removeBackground } from '$lib/utils/professionalImageProcessing';
import { extractPicletMetadata } from '$lib/services/picletMetadata';
import { savePicletInstance, generatedDataToPicletInstance } from '$lib/db/piclets';
import { PicletType, TYPE_DATA } from '$lib/types/picletTypes';
import { EnhancedCaptionService } from '$lib/services/enhancedCaption';
import { CanonicalService } from '$lib/services/canonicalService';
import { incrementDiscoveryCounter, addRarityScore, calculateRarityPoints } from '$lib/db/gameState';
import { authStore } from '$lib/stores/auth';
// import { withQwenTimeout } from '$lib/utils/qwenTimeout'; // Unused since qwen is disabled
interface Props extends PicletGeneratorProps {}
let {
joyCaptionClient,
fluxClient,
gptOssClient,
picletsServerClient
}: Props = $props();
// Get current user info for discoverer attribution
const auth = $derived(authStore);
// GPT-OSS-120B text generation
const generateText = async (prompt: string): Promise{
if (!gptOssClient) {
throw new Error('GPT-OSS-120B client is not available');
}
console.log('Generating text with GPT-OSS-120B...');
// ChatInterface expects: message, history, system_prompt, temperature
const result = await gptOssClient.predict("/chat", [
prompt, // message
[], // history
"You are a helpful assistant that creates Pokemon-style monster concepts based on real-world objects.", // system_prompt
0.7 // temperature
]);
// Extract Response section only (GPT-OSS formats with Analysis and Response)
let responseText = result.data[0] || '';
const responseMatch = responseText.match(/\*\*💬 Response:\*\*\s*\n\n([\s\S]*)/);
if (responseMatch) {
return responseMatch[1].trim();
}
// Fallback: extract after "assistantfinal"
const finalMatch = responseText.match(/assistantfinal\s*([\s\S]*)/);
if (finalMatch) {
return finalMatch[1].trim();
}
return responseText;
};
let workflowState: PicletWorkflowState = $state({
currentStep: 'upload',
userImage: null,
imageCaption: null,
picletConcept: null,
picletStats: null,
imagePrompt: null,
picletImage: null,
error: null,
isProcessing: false,
// Discovery-specific state
objectName: null,
objectAttributes: [],
visualDetails: null,
discoveryStatus: null,
canonicalPiclet: null
});
// Queue state for multi-image processing
let imageQueue: File[] = $state([]);
let currentImageIndex: number = $state(0);
const IMAGE_GENERATION_PROMPT = (concept: string) => `Extract ONLY the visual appearance from this monster concept and describe it in one concise sentence:
"${concept}"
Focus on: colors, body shape, eyes, limbs, mouth, and key visual features. Omit backstory, abilities, and non-visual details.`;
async function importPiclet(picletData: PicletInstance) {
workflowState.isProcessing = true;
workflowState.currentStep = 'complete';
try {
// Save the imported piclet
const savedId = await savePicletInstance(picletData);
// Create a success workflowState similar to generation
workflowState.picletImage = {
imageUrl: picletData.imageUrl,
imageData: picletData.imageData,
seed: 0,
prompt: 'Imported piclet'
};
// Show import success
workflowState.isProcessing = false;
alert(`Successfully imported ${picletData.nickname || picletData.typeId}!`);
// Reset to allow another import/generation
setTimeout(() => reset(), 2000);
} catch (error) {
workflowState.error = `Failed to import piclet: ${error}`;
workflowState.isProcessing = false;
}
}
async function handleImageSelected(file: File) {
if (!joyCaptionClient || !fluxClient) {
workflowState.error = "Services not connected. Please wait...";
return;
}
// Single image upload - clear queue and process normally
imageQueue = [];
currentImageIndex = 0;
workflowState.userImage = file;
workflowState.error = null;
// Check if this is a piclet card with metadata
const picletData = await extractPicletMetadata(file);
if (picletData) {
// Import existing piclet
await importPiclet(picletData);
} else {
// Generate new piclet
startWorkflow();
}
}
async function handleImagesSelected(files: File[]) {
if (!joyCaptionClient || !fluxClient) {
workflowState.error = "Services not connected. Please wait...";
return;
}
// Multi-image upload - set up queue and start with first image
imageQueue = files;
currentImageIndex = 0;
await processCurrentImage();
}
async function processCurrentImage() {
if (currentImageIndex >= imageQueue.length) {
// Queue completed
console.log('All images processed!');
return;
}
const currentFile = imageQueue[currentImageIndex];
workflowState.userImage = currentFile;
workflowState.error = null;
// Check if this is a piclet card with metadata
const picletData = await extractPicletMetadata(currentFile);
if (picletData) {
// Import existing piclet
await importPiclet(picletData);
// Auto-advance to next image after import
await advanceToNextImage();
} else {
// Generate new piclet
startWorkflow();
}
}
async function advanceToNextImage() {
currentImageIndex++;
if (currentImageIndex < imageQueue.length) {
// Process next image
setTimeout(() => processCurrentImage(), 1000); // Small delay for better UX
} else {
// Queue completed - reset to single image mode
imageQueue = [];
currentImageIndex = 0;
reset();
}
}
async function startWorkflow() {
workflowState.isProcessing = true;
try {
// Step 1: Generate detailed object description with joy-caption (skip server lookup for now)
await captionImage();
await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for workflowState update
// Step 2: Generate free-form monster concept with Qwen
await generateConcept();
await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for workflowState update
// Step 3: Extract stats including physical characteristics
await extractSimpleStats();
await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for workflowState update
// Step 4: Generate image prompt with Qwen
await generateImagePrompt();
await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for workflowState update
// Step 5: Generate monster image with anime style
await generateMonsterImage();
// Step 6: Auto-save the piclet as caught (since scanning now auto-captures)
await autoSavePicletAsCaught();
workflowState.currentStep = 'complete';
// If processing a queue, auto-advance to next image after a short delay
if (imageQueue.length > 1) {
setTimeout(() => advanceToNextImage(), 2000); // 2 second delay to show completion
}
} catch (err) {
console.error('Workflow error:', err);
// Check for GPU quota error
if (err && typeof err === 'object' && 'message' in err) {
const errorMessage = String(err.message);
if (errorMessage.includes('exceeded your GPU quota') || errorMessage.includes('GPU quota')) {
workflowState.error = 'GPU quota exceeded! You need to sign in with Hugging Face for free GPU time, or upgrade to Hugging Face Pro for more quota.';
} else {
workflowState.error = errorMessage;
}
} else if (err instanceof Error) {
workflowState.error = err.message;
} else {
workflowState.error = 'An unknown error occurred';
}
} finally {
workflowState.isProcessing = false;
}
}
function handleAPIError(error: any): never {
console.error('API Error:', error);
// Check if it's a GPU quota error
if (error && typeof error === 'object' && 'message' in error) {
const errorMessage = String(error.message);
if (errorMessage.includes('exceeded your GPU quota') || errorMessage.includes('GPU quota')) {
throw new Error('GPU quota exceeded! You need to sign in with Hugging Face for free GPU time, or upgrade to Hugging Face Pro for more quota.');
}
throw new Error(errorMessage);
}
// Check if error has a different structure (like the status object from the logs)
if (error && typeof error === 'object' && 'type' in error && error.type === 'status') {
const statusError = error as any;
if (statusError.message && statusError.message.includes('GPU quota')) {
throw new Error('GPU quota exceeded! You need to sign in with Hugging Face for free GPU time, or upgrade to Hugging Face Pro for more quota.');
}
throw new Error(statusError.message || 'API request failed');
}
throw error;
}
async function captionImage() {
workflowState.currentStep = 'captioning';
if (!joyCaptionClient || !workflowState.userImage) {
throw new Error('Caption service not available or no image provided');
}
try {
// Get detailed scene description from Joy Caption
const captionResult = await EnhancedCaptionService.generateEnhancedCaption(
joyCaptionClient,
workflowState.userImage
);
workflowState.imageCaption = captionResult.caption;
console.log('Scene description:', captionResult.caption);
// Skip server lookup for now - always create new piclet
workflowState.discoveryStatus = 'new';
} catch (error) {
handleAPIError(error);
}
}
async function generateConcept() {
workflowState.currentStep = 'conceptualizing';
// Skip if we have an existing canonical Piclet
if (workflowState.discoveryStatus === 'existing' && workflowState.canonicalPiclet) {
workflowState.picletConcept = workflowState.canonicalPiclet.concept;
console.log('Using existing canonical concept');
return;
}
if (!gptOssClient || !workflowState.imageCaption) {
throw new Error('Cannot generate concept without scene description');
}
const conceptPrompt = `You are analyzing an image to create a Pokemon-style creature. Here's the image description:
"${workflowState.imageCaption}"
Your task:
1. Identify the PRIMARY PHYSICAL OBJECT with SPECIFICITY (e.g., "macbook" not "laptop", "eiffel tower" not "tower", "iphone" not "phone", "starbucks mug" not "mug")
2. Determine if there's a meaningful VARIATION (e.g., "silver", "pro", "night", "gaming", "vintage")
3. Assess rarity based on uniqueness
4. Create a complete Pokemon-style monster concept
Format your response EXACTLY as follows:
\`\`\`md
# Canonical Object
{Specific object name: "macbook", "eiffel tower", "iphone", "tesla", "le creuset mug", "nintendo switch"}
{NOT generic terms like: "laptop", "tower", "phone", "car", "mug", "console"}
{Include brand/model/landmark name when identifiable}
# Variation
{OPTIONAL: one distinctive attribute like "silver", "pro", "night", "gaming", OR use "canonical" if this is the standard/default version with no special variation}
# Object Rarity
{common, uncommon, rare, epic, or legendary based on object uniqueness}
# Monster Name
{Creative 8-11 letter name based on the SPECIFIC object, e.g., "Macbyte" for MacBook, "Towerfell" for Eiffel Tower}
# Primary Type
{beast, bug, aquatic, flora, mineral, space, machina, structure, culture, or cuisine}
# Physical Stats
Height: {e.g., "1.2m" or "3'5\""}
Weight: {e.g., "15kg" or "33 lbs"}
# Personality
{1-2 sentences describing personality traits}
# Monster Description
{2-3 paragraphs describing how the SPECIFIC object's features translate into monster features. Reference the actual object by name. This is the creature's bio.}
# Monster Image Prompt
{Concise visual description for anime-style image generation focusing on colors, shapes, and key features inspired by the specific object}
\`\`\`
CRITICAL RULES:
- Canonical Object MUST be SPECIFIC: "macbook" not "laptop", "big ben" not "clock tower", "coca cola" not "soda"
- If you can identify a brand, model, or proper name from the description, USE IT
- Variation should be meaningful and distinctive (material, style, color, context, or model variant)
- Monster Description must describe the CREATURE with references to the specific object's features
- Primary Type must match the object category (machina for electronics, structure for buildings, etc.)`;
try {
const responseText = await generateText(conceptPrompt);
// Validate response has expected structure
if (!responseText.includes('# Canonical Object') ||
!responseText.includes('# Monster Name')) {
console.error('GPT-OSS returned invalid response:', responseText);
throw new Error('Failed to generate valid monster concept');
}
workflowState.picletConcept = responseText;
// Extract and store canonical name and variation immediately for use in other steps
// Handle both plain and bold markdown headers
const canonicalMatch = responseText.match(/\*{0,2}#\s*Canonical Object\s*\*{0,2}\s*\n([\s\S]*?)(?=^\*{0,2}#)/m);
const variationMatch = responseText.match(/\*{0,2}#\s*Variation\s*\*{0,2}\s*\n([\s\S]*?)(?=^\*{0,2}#)/m);
// Clean up extracted values (remove curly braces and quotes that GPT-OSS sometimes adds)
let objectName = canonicalMatch ? canonicalMatch[1].trim().toLowerCase() : 'unknown';
objectName = objectName.replace(/^[{"]|["}]$/g, '').replace(/^.*:\s*["']|["']$/g, '').trim();
let variationText = variationMatch ? variationMatch[1].trim() : '';
variationText = variationText.replace(/^[{"]|["}]$/g, '').replace(/^.*:\s*["']|["']$/g, '').trim();
workflowState.objectName = objectName;
workflowState.objectAttributes = variationText && variationText !== 'NONE' && variationText !== 'canonical' ? [variationText.toLowerCase()] : [];
console.log('Parsed specific object:', workflowState.objectName);
console.log('Parsed variation:', workflowState.objectAttributes);
if (!responseText || responseText.trim() === '') {
throw new Error('Failed to generate monster concept');
}
// Parse markdown code block response
let cleanedResponse = responseText.trim();
// Check if response is wrapped in markdown code blocks
if (cleanedResponse.includes('```')) {
// Handle different code block formats: ```md, ```, ```markdown
const codeBlockRegex = /```(?:md|markdown)?\s*\n([\s\S]*?)```/;
const match = cleanedResponse.match(codeBlockRegex);
if (match && match[1]) {
cleanedResponse = match[1].trim();
console.log('Extracted content from markdown code block');
} else {
// Fallback: try to extract content between any ``` blocks
const simpleMatch = cleanedResponse.match(/```([\s\S]*?)```/);
if (simpleMatch && simpleMatch[1]) {
cleanedResponse = simpleMatch[1].trim();
console.log('Extracted content from generic code block');
}
}
}
// Ensure the response contains expected markdown headers
if (!cleanedResponse.includes('# Object Rarity') || !cleanedResponse.includes('# Monster Name') || !cleanedResponse.includes('# Monster Image Prompt')) {
console.warn('Response does not contain expected markdown structure (missing Object Rarity, Monster Name, or Monster Image Prompt)');
}
workflowState.picletConcept = cleanedResponse;
console.log('Monster concept generated:', cleanedResponse);
} catch (error) {
handleAPIError(error);
}
}
async function generateImagePrompt() {
workflowState.currentStep = 'promptCrafting';
if (!gptOssClient || !workflowState.picletConcept || !workflowState.imageCaption) {
throw new Error('Text generation service not available or no concept/caption available for prompt generation');
}
// Extract the Monster Image Prompt from the structured concept (handle both plain and bold markdown headers)
const imagePromptMatch = workflowState.picletConcept.match(/\*{0,2}#\s*Monster Image Prompt\s*\*{0,2}\s*\n([\s\S]*?)(?=^\*{0,2}#|$)/m);
if (imagePromptMatch && imagePromptMatch[1]) {
workflowState.imagePrompt = imagePromptMatch[1].trim();
console.log('Extracted image prompt for generation:', workflowState.imagePrompt);
return; // Skip fallback call since we have the prompt
}
// Fallback: if format parsing fails, use Qwen to extract visual description
const imagePromptPrompt = `Based on this monster concept, extract ONLY the visual description for image generation:
MONSTER CONCEPT:
"""
${workflowState.picletConcept}
"""
Create a concise visual description (1-3 sentences, max 100 words). Focus only on colors, shapes, materials, eyes, limbs, mouth, and distinctive features. Omit all non-visual information like abilities and backstory.`;
try {
const responseText = await generateText(imagePromptPrompt);
if (!responseText || responseText.trim() === '') {
throw new Error('Failed to generate image prompt');
}
workflowState.imagePrompt = responseText.trim();
console.log('Image prompt generated:', workflowState.imagePrompt);
} catch (error) {
handleAPIError(error);
}
}
async function generateMonsterImage() {
workflowState.currentStep = 'generating';
if (!fluxClient || !workflowState.imagePrompt || !workflowState.picletStats) {
throw new Error('Image generation service not available or no prompt/stats');
}
// The image prompt should already be generated by generateImagePrompt() in the workflow
// Get tier for image quality enhancement
const tier = workflowState.picletStats.tier || 'medium';
const tierDescriptions = {
low: 'simple and iconic design',
medium: 'detailed and well-crafted design',
high: 'highly detailed and impressive design with special effects',
legendary: 'highly detailed and majestic design with dramatic lighting and aura effects'
};
try {
const output = await fluxClient.predict("/infer", [
`${workflowState.imagePrompt}\nNow generate an Pokémon Anime image of the monster in an idle pose with a plain dark-grey background. This is a ${tier} tier monster with a ${tierDescriptions[tier as keyof typeof tierDescriptions]}. The monster should not be attacking or in motion. The full monster must be visible within the frame.`,
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) {
// Process the image to remove background using professional AI method
console.log('Processing image for background removal...');
try {
const transparentBase64 = await removeBackground(url);
workflowState.picletImage = {
imageUrl: url,
imageData: transparentBase64,
seed: usedSeed,
prompt: workflowState.imagePrompt
};
console.log('Background removal completed successfully');
} catch (processError) {
console.error('Failed to process image for background removal:', processError);
// Fallback to original image
workflowState.picletImage = {
imageUrl: url,
seed: usedSeed,
prompt: workflowState.imagePrompt
};
}
} else {
throw new Error('Failed to generate monster image');
}
} catch (error) {
handleAPIError(error);
}
}
async function extractSimpleStats() {
workflowState.currentStep = 'statsGenerating';
if (!workflowState.picletConcept) {
throw new Error('No concept available for stats extraction');
}
try {
// Extract monster name (handle both plain and bold markdown headers)
const monsterNameMatch = workflowState.picletConcept.match(/\*{0,2}#\s*Monster Name\s*\*{0,2}\s*\n([\s\S]*?)(?=^\*{0,2}#|$)/m);
let monsterName = monsterNameMatch ? monsterNameMatch[1].trim() : 'Unknown Monster';
// Clean and truncate name
monsterName = monsterName.replace(/^[{"]|["}]$/g, '').trim(); // Remove curly braces and quotes
if (monsterName.includes(',')) {
monsterName = monsterName.split(',')[0].trim();
}
if (monsterName.length > 12) {
monsterName = monsterName.substring(0, 12);
}
monsterName = monsterName.replace(/\*/g, ''); // Remove markdown asterisks
// Extract rarity and convert to tier (handle both plain and bold markdown headers)
const rarityMatch = workflowState.picletConcept.match(/\*{0,2}#\s*Object Rarity\s*\*{0,2}\s*\n([\s\S]*?)(?=^\*{0,2}#)/m);
const objectRarity = rarityMatch ? rarityMatch[1].trim().toLowerCase() : 'common';
let tier: 'low' | 'medium' | 'high' | 'legendary' = 'medium';
if (objectRarity.includes('common')) tier = 'low';
else if (objectRarity.includes('uncommon')) tier = 'medium';
else if (objectRarity.includes('rare')) tier = 'high';
else if (objectRarity.includes('legendary') || objectRarity.includes('mythical')) tier = 'legendary';
// Extract primary type (handle both plain and bold markdown headers)
const primaryTypeMatch = workflowState.picletConcept.match(/\*{0,2}#\s*Primary Type\s*\*{0,2}\s*\n([\s\S]*?)(?=^\*{0,2}#|$)/m);
let primaryType: any = primaryTypeMatch ? primaryTypeMatch[1].trim().toLowerCase() : 'beast';
primaryType = primaryType.replace(/^[{"]|["}]$/g, '').trim(); // Remove curly braces and quotes
// Extract description (handle both plain and bold markdown headers)
const descriptionMatch = workflowState.picletConcept.match(/\*{0,2}#\s*Monster Description\s*\*{0,2}\s*\n([\s\S]*?)(?=^\*{0,2}#|$)/m);
if (!descriptionMatch) {
console.error('Monster description not found in concept:', workflowState.picletConcept);
throw new Error('Failed to extract monster description from AI response');
}
let description = descriptionMatch[1].trim();
// Extract physical stats (handle both plain and bold markdown headers)
const physicalStatsMatch = workflowState.picletConcept.match(/\*{0,2}#\s*Physical Stats\s*\*{0,2}\s*\n([\s\S]*?)(?=^\*{0,2}#|$)/m);
let height: string | undefined;
let weight: string | undefined;
if (physicalStatsMatch) {
const physicalStatsText = physicalStatsMatch[1];
const heightMatch = physicalStatsText.match(/Height:\s*(.+)/i);
const weightMatch = physicalStatsText.match(/Weight:\s*(.+)/i);
height = heightMatch ? heightMatch[1].trim().replace(/^[{"]|["}]$/g, '').trim() : undefined;
weight = weightMatch ? weightMatch[1].trim().replace(/^[{"]|["}]$/g, '').trim() : undefined;
}
// Extract personality (handle both plain and bold markdown headers)
const personalityMatch = workflowState.picletConcept.match(/\*{0,2}#\s*Personality\s*\*{0,2}\s*\n([\s\S]*?)(?=^\*{0,2}#|$)/m);
let personality = personalityMatch ? personalityMatch[1].trim().replace(/^[{"]|["}]$/g, '').trim() : undefined;
// Create stats with physical characteristics
const stats: PicletStats = {
name: monsterName,
description: description,
tier: tier,
primaryType: primaryType,
height,
weight,
personality
};
workflowState.picletStats = stats;
console.log('Stats extracted:', stats);
} catch (error) {
console.error('Failed to extract stats:', error);
handleAPIError(error);
}
}
async function autoSavePicletAsCaught() {
if (!workflowState.picletImage || !workflowState.imageCaption || !workflowState.picletConcept || !workflowState.imagePrompt || !workflowState.picletStats) {
console.error('Cannot auto-save: missing required data');
return;
}
try {
// Create a clean copy of stats to ensure it's serializable
const cleanStats = JSON.parse(JSON.stringify(workflowState.picletStats));
const picletData = {
name: workflowState.picletStats.name,
imageUrl: workflowState.picletImage.imageUrl,
imageData: workflowState.picletImage.imageData,
imageCaption: workflowState.imageCaption,
concept: workflowState.picletConcept,
imagePrompt: workflowState.imagePrompt,
stats: cleanStats,
createdAt: new Date()
};
// Check for any non-serializable data
console.log('Checking piclet data for serializability:');
console.log('- name type:', typeof picletData.name);
console.log('- imageUrl type:', typeof picletData.imageUrl);
console.log('- imageData type:', typeof picletData.imageData, picletData.imageData ? `length: ${picletData.imageData.length}` : 'null/undefined');
console.log('- imageCaption type:', typeof picletData.imageCaption);
console.log('- concept type:', typeof picletData.concept);
console.log('- imagePrompt type:', typeof picletData.imagePrompt);
console.log('- stats:', cleanStats);
// Convert to PicletInstance format and save as caught
// Convert reactive Svelte state to plain values (removes Proxy wrapper for IndexedDB compatibility)
const plainAttributes = workflowState.objectAttributes ? [...workflowState.objectAttributes] : [];
const picletInstance = await generatedDataToPicletInstance(
picletData,
workflowState.objectName || undefined,
plainAttributes,
workflowState.visualDetails || undefined,
$auth.userInfo // Pass user info for discoverer attribution
);
// Sync with server if available and user is authenticated
if (picletsServerClient && $auth.session?.accessToken && workflowState.objectName) {
try {
console.log('Syncing piclet to server...', workflowState.objectName);
// Search for existing canonical on server
const searchResult = await CanonicalService.searchCanonical(
picletsServerClient,
workflowState.objectName,
plainAttributes
);
console.log('Server search result:', searchResult?.status);
if (searchResult?.status === 'new') {
// Create new canonical on server
console.log('Creating canonical piclet on server...');
const serverResult = await CanonicalService.createCanonical(
picletsServerClient,
workflowState.objectName,
picletInstance,
$auth.session.accessToken
);
console.log('Server canonical creation result:', serverResult);
} else if (searchResult?.status === 'new_variation' && searchResult.canonicalId) {
// Create variation on server
console.log('Creating variation on server...');
const serverResult = await CanonicalService.createVariation(
picletsServerClient,
searchResult.canonicalId,
workflowState.objectName,
plainAttributes,
picletInstance,
$auth.session.accessToken
);
console.log('Server variation creation result:', serverResult);
} else if (searchResult?.status === 'existing' || searchResult?.status === 'variation') {
// Increment scan count for existing piclet
const picletId = searchResult.piclet?.typeId || searchResult.canonicalId;
if (picletId) {
console.log('Incrementing scan count on server...');
await CanonicalService.incrementScanCount(
picletsServerClient,
picletId,
workflowState.objectName
);
}
}
} catch (serverError) {
console.error('Server sync failed (continuing with local save):', serverError);
// Don't throw - continue with local save even if server fails
}
}
const picletId = await savePicletInstance(picletInstance);
console.log('Piclet auto-saved as caught with ID:', picletId);
// Update game state statistics
await incrementDiscoveryCounter('totalDiscoveries');
// Update canonical vs variation counters
if (picletInstance.isCanonical) {
await incrementDiscoveryCounter('uniqueDiscoveries');
} else {
await incrementDiscoveryCounter('variationsFound');
}
// Calculate and add rarity score
const rarityPoints = calculateRarityPoints(picletInstance.scanCount);
await addRarityScore(rarityPoints);
console.log('Game state updated: +1 discovery, +' + rarityPoints + ' rarity points');
} catch (err) {
console.error('Failed to auto-save piclet:', err);
console.error('Piclet data that failed to save:', {
name: workflowState.picletStats?.name,
hasImageUrl: !!workflowState.picletImage?.imageUrl,
hasImageData: !!workflowState.picletImage?.imageData,
hasStats: !!workflowState.picletStats
});
// Don't throw - we don't want to interrupt the workflow
}
}
function reset() {
workflowState = {
currentStep: 'upload',
userImage: null,
imageCaption: null,
picletConcept: null,
picletStats: null,
imagePrompt: null,
picletImage: null,
error: null,
isProcessing: false,
objectName: null,
objectAttributes: [],
visualDetails: null,
discoveryStatus: null,
canonicalPiclet: null
};
}
</script>
<div class="piclet-generator">
{#if workflowState.currentStep !== 'upload'}
<WorkflowProgress currentStep={workflowState.currentStep} error={workflowState.error} />
{/if}
{#if workflowState.currentStep === 'upload'}
<UploadStep
onImageSelected={handleImageSelected}
onImagesSelected={handleImagesSelected}
isProcessing={workflowState.isProcessing}
imageQueue={imageQueue}
currentImageIndex={currentImageIndex}
/>
{:else if workflowState.currentStep === 'complete'}
<PicletResult workflowState={workflowState} onReset={reset} />
{:else}
<div class="processing-container">
<div class="spinner"></div>
<p class="processing-text">
{#if workflowState.currentStep === 'captioning'}
Analyzing your image...
{:else if workflowState.currentStep === 'conceptualizing'}
Creating Piclet concept...
{:else if workflowState.currentStep === 'statsGenerating'}
Generating piclet characteristics...
{:else if workflowState.currentStep === 'promptCrafting'}
Creating image prompt...
{:else if workflowState.currentStep === 'generating'}
Generating your Piclet...
{/if}
</p>
</div>
{/if}
</div>
<style>
.piclet-generator {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
/* Client selector styles (hidden since only HunyuanTurbos is active) */
/*
.client-selector {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
padding: 0.75rem;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.client-selector label {
font-weight: 500;
color: #495057;
}
.client-selector select {
padding: 0.25rem 0.5rem;
border: 1px solid #ced4da;
border-radius: 4px;
background: white;
color: #495057;
font-size: 0.9rem;
}
*/
.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;
}
</style> |