Fraser commited on
Commit
d9c705e
·
1 Parent(s): 303b4b7

scanner DB update

Browse files
src/lib/components/MonsterGenerator/MonsterGenerator.svelte CHANGED
@@ -3,6 +3,7 @@
3
  import UploadStep from './UploadStep.svelte';
4
  import WorkflowProgress from './WorkflowProgress.svelte';
5
  import MonsterResult from './MonsterResult.svelte';
 
6
 
7
  interface Props extends MonsterGeneratorProps {}
8
 
@@ -50,6 +51,40 @@ Include all of its visual details, format the description as a single long sente
50
 
51
  Assistant:`;
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  async function handleImageSelected(file: File) {
54
  if (!joyCaptionClient || !rwkvClient || !fluxClient) {
55
  state.error = "Services not connected. Please wait...";
@@ -73,11 +108,15 @@ Assistant:`;
73
  await generateMonsterConcept();
74
  await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for state update
75
 
76
- // Step 3: Generate image prompt
 
 
 
 
77
  await generateImagePrompt();
78
  await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for state update
79
 
80
- // Step 4: Generate monster image
81
  await generateMonsterImage();
82
 
83
  state.currentStep = 'complete';
@@ -242,11 +281,25 @@ Assistant:`;
242
  else if (image && image.path) url = image.path;
243
 
244
  if (url) {
245
- state.monsterImage = {
246
- imageUrl: url,
247
- seed: usedSeed,
248
- prompt: state.imagePrompt
249
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  } else {
251
  throw new Error('Failed to generate monster image');
252
  }
@@ -255,12 +308,59 @@ Assistant:`;
255
  }
256
  }
257
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  function reset() {
259
  state = {
260
  currentStep: 'upload',
261
  userImage: null,
262
  imageCaption: null,
263
  monsterConcept: null,
 
264
  imagePrompt: null,
265
  monsterImage: null,
266
  error: null,
@@ -290,6 +390,8 @@ Assistant:`;
290
  Analyzing your image...
291
  {:else if state.currentStep === 'conceptualizing'}
292
  Creating monster concept...
 
 
293
  {:else if state.currentStep === 'promptCrafting'}
294
  Crafting generation prompt...
295
  {:else if state.currentStep === 'generating'}
 
3
  import UploadStep from './UploadStep.svelte';
4
  import WorkflowProgress from './WorkflowProgress.svelte';
5
  import MonsterResult from './MonsterResult.svelte';
6
+ import { makeWhiteTransparent } from '$lib/utils/imageProcessing';
7
 
8
  interface Props extends MonsterGeneratorProps {}
9
 
 
51
 
52
  Assistant:`;
53
 
54
+ const MONSTER_STATS_PROMPT = (concept: string) => `User: Convert the following monster concept into a JSON object with stats:
55
+
56
+ "${concept}"
57
+
58
+ The output should be formatted as a JSON instance that conforms to the JSON schema below.
59
+
60
+ As an example, for the schema {"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]}
61
+ the object {"foo": ["bar", "baz"]} is a well-formatted instance of the schema. The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted.
62
+
63
+ Here is the output schema:
64
+ \`\`\`
65
+ {
66
+ "properties": {
67
+ "name": {"type": "string", "description": "The monster's name"},
68
+ "description": {"type": "string", "description": "A brief description of the monster"},
69
+ "rarity": {"type": "string", "enum": ["very-low", "low", "medium", "high", "very-high"], "description": "How rare/unique the monster is"},
70
+ "HP": {"type": "string", "enum": ["very-low", "low", "medium", "high", "very-high"], "description": "Health points"},
71
+ "defence": {"type": "string", "enum": ["very-low", "low", "medium", "high", "very-high"], "description": "Defensive capability"},
72
+ "attack": {"type": "string", "enum": ["very-low", "low", "medium", "high", "very-high"], "description": "Attack power"},
73
+ "speed": {"type": "string", "enum": ["very-low", "low", "medium", "high", "very-high"], "description": "Movement speed"},
74
+ "specialAbility": {"type": "string", "description": "Passive trait that gives the monster a unique advantage in battle"},
75
+ "attackActionDescription": {"type": "string", "description": "Primary attack that deals damage"},
76
+ "boostActionDescription": {"type": "string", "description": "Action that buffs the monster's own stats/status"},
77
+ "disparageActionDescription": {"type": "string", "description": "Action that lowers enemy stats/status"},
78
+ "specialActionDescription": {"type": "string", "description": "Powerful action with single use per battle"}
79
+ },
80
+ "required": ["name", "description", "rarity", "HP", "defence", "attack", "speed", "specialAbility", "attackActionDescription", "boostActionDescription", "disparageActionDescription", "specialActionDescription"]
81
+ }
82
+ \`\`\`
83
+
84
+ Remember to base the stats on how unique/powerful the original object was. Common objects should have lower stats, unique objects should have higher stats.
85
+
86
+ Assistant: \`\`\`json`;
87
+
88
  async function handleImageSelected(file: File) {
89
  if (!joyCaptionClient || !rwkvClient || !fluxClient) {
90
  state.error = "Services not connected. Please wait...";
 
108
  await generateMonsterConcept();
109
  await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for state update
110
 
111
+ // Step 3: Generate monster stats
112
+ await generateStats();
113
+ await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for state update
114
+
115
+ // Step 4: Generate image prompt
116
  await generateImagePrompt();
117
  await new Promise(resolve => setTimeout(resolve, 100)); // Small delay for state update
118
 
119
+ // Step 5: Generate monster image
120
  await generateMonsterImage();
121
 
122
  state.currentStep = 'complete';
 
281
  else if (image && image.path) url = image.path;
282
 
283
  if (url) {
284
+ // Process the image to make white background transparent
285
+ console.log('Processing image for transparency...');
286
+ try {
287
+ const transparentBase64 = await makeWhiteTransparent(url);
288
+ state.monsterImage = {
289
+ imageUrl: url,
290
+ imageData: transparentBase64,
291
+ seed: usedSeed,
292
+ prompt: state.imagePrompt
293
+ };
294
+ } catch (processError) {
295
+ console.error('Failed to process image for transparency:', processError);
296
+ // Fallback to original image
297
+ state.monsterImage = {
298
+ imageUrl: url,
299
+ seed: usedSeed,
300
+ prompt: state.imagePrompt
301
+ };
302
+ }
303
  } else {
304
  throw new Error('Failed to generate monster image');
305
  }
 
308
  }
309
  }
310
 
311
+ async function generateStats() {
312
+ state.currentStep = 'statsGenerating';
313
+
314
+ if (!rwkvClient || !state.monsterConcept) {
315
+ throw new Error('Text generation service not available or no concept');
316
+ }
317
+
318
+ const statsPrompt = MONSTER_STATS_PROMPT(state.monsterConcept);
319
+
320
+ console.log('Generating monster stats from concept');
321
+
322
+ try {
323
+ const output = await rwkvClient.predict(0, [
324
+ statsPrompt,
325
+ 500, // maxTokens - more for JSON
326
+ 0.3, // temperature - lower for structured output
327
+ 0.9, // topP
328
+ 0.05, // presencePenalty - lower for JSON
329
+ 0.05 // countPenalty
330
+ ]);
331
+
332
+ console.log('Stats output:', output);
333
+ const jsonString = output.data[0];
334
+
335
+ // Extract JSON from the response (remove markdown if present)
336
+ let cleanJson = jsonString;
337
+ if (jsonString.includes('```')) {
338
+ const matches = jsonString.match(/```(?:json)?\s*([\s\S]*?)```/);
339
+ if (matches) {
340
+ cleanJson = matches[1];
341
+ }
342
+ }
343
+
344
+ try {
345
+ const stats: MonsterStats = JSON.parse(cleanJson.trim());
346
+ state.monsterStats = stats;
347
+ console.log('Monster stats generated:', stats);
348
+ } catch (parseError) {
349
+ console.error('Failed to parse JSON:', parseError, 'Raw output:', cleanJson);
350
+ throw new Error('Failed to parse monster stats JSON');
351
+ }
352
+ } catch (error) {
353
+ handleAPIError(error);
354
+ }
355
+ }
356
+
357
  function reset() {
358
  state = {
359
  currentStep: 'upload',
360
  userImage: null,
361
  imageCaption: null,
362
  monsterConcept: null,
363
+ monsterStats: null,
364
  imagePrompt: null,
365
  monsterImage: null,
366
  error: null,
 
390
  Analyzing your image...
391
  {:else if state.currentStep === 'conceptualizing'}
392
  Creating monster concept...
393
+ {:else if state.currentStep === 'statsGenerating'}
394
+ Generating battle stats...
395
  {:else if state.currentStep === 'promptCrafting'}
396
  Crafting generation prompt...
397
  {:else if state.currentStep === 'generating'}
src/lib/components/MonsterGenerator/MonsterResult.svelte CHANGED
@@ -13,10 +13,11 @@
13
  let saveError: string | null = $state(null);
14
 
15
  function downloadImage() {
16
- if (!workflowState.monsterImage?.imageUrl) return;
17
 
18
  const link = document.createElement('a');
19
- link.href = workflowState.monsterImage.imageUrl;
 
20
  link.download = `monster-${Date.now()}.png`;
21
  link.click();
22
  }
@@ -51,12 +52,19 @@
51
  }
52
  }
53
 
 
 
 
 
 
54
  await saveMonster({
55
  name: monsterName,
56
  imageUrl: workflowState.monsterImage.imageUrl,
 
57
  imageCaption: workflowState.imageCaption,
58
  concept: workflowState.monsterConcept,
59
- imagePrompt: workflowState.imagePrompt
 
60
  });
61
 
62
  isSaved = true;
@@ -75,7 +83,7 @@
75
  {#if workflowState.monsterImage}
76
  <div class="monster-image-container">
77
  <img
78
- src={workflowState.monsterImage.imageUrl}
79
  alt="Generated Monster"
80
  class="monster-image"
81
  />
@@ -111,6 +119,56 @@
111
  {/if}
112
  </div>
113
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  </div>
115
 
116
  <div class="action-buttons">
@@ -302,6 +360,65 @@
302
  to { transform: rotate(360deg); }
303
  }
304
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  @media (max-width: 768px) {
306
  .result-container {
307
  padding: 1rem;
 
13
  let saveError: string | null = $state(null);
14
 
15
  function downloadImage() {
16
+ if (!workflowState.monsterImage) return;
17
 
18
  const link = document.createElement('a');
19
+ // Use transparent image if available, otherwise original
20
+ link.href = workflowState.monsterImage.imageData || workflowState.monsterImage.imageUrl;
21
  link.download = `monster-${Date.now()}.png`;
22
  link.click();
23
  }
 
52
  }
53
  }
54
 
55
+ // Use stats name if available, otherwise use extracted name
56
+ if (workflowState.monsterStats?.name) {
57
+ monsterName = workflowState.monsterStats.name;
58
+ }
59
+
60
  await saveMonster({
61
  name: monsterName,
62
  imageUrl: workflowState.monsterImage.imageUrl,
63
+ imageData: workflowState.monsterImage.imageData,
64
  imageCaption: workflowState.imageCaption,
65
  concept: workflowState.monsterConcept,
66
+ imagePrompt: workflowState.imagePrompt,
67
+ stats: workflowState.monsterStats || undefined
68
  });
69
 
70
  isSaved = true;
 
83
  {#if workflowState.monsterImage}
84
  <div class="monster-image-container">
85
  <img
86
+ src={workflowState.monsterImage.imageData || workflowState.monsterImage.imageUrl}
87
  alt="Generated Monster"
88
  class="monster-image"
89
  />
 
119
  {/if}
120
  </div>
121
  </div>
122
+
123
+ {#if workflowState.monsterStats}
124
+ <div class="result-section">
125
+ <h4>Battle Stats</h4>
126
+ <div class="stats-grid">
127
+ <div class="stat-item">
128
+ <span class="stat-label">Rarity:</span>
129
+ <span class="stat-value stat-{workflowState.monsterStats.rarity}">{workflowState.monsterStats.rarity}</span>
130
+ </div>
131
+ <div class="stat-item">
132
+ <span class="stat-label">HP:</span>
133
+ <span class="stat-value stat-{workflowState.monsterStats.HP}">{workflowState.monsterStats.HP}</span>
134
+ </div>
135
+ <div class="stat-item">
136
+ <span class="stat-label">Attack:</span>
137
+ <span class="stat-value stat-{workflowState.monsterStats.attack}">{workflowState.monsterStats.attack}</span>
138
+ </div>
139
+ <div class="stat-item">
140
+ <span class="stat-label">Defence:</span>
141
+ <span class="stat-value stat-{workflowState.monsterStats.defence}">{workflowState.monsterStats.defence}</span>
142
+ </div>
143
+ <div class="stat-item">
144
+ <span class="stat-label">Speed:</span>
145
+ <span class="stat-value stat-{workflowState.monsterStats.speed}">{workflowState.monsterStats.speed}</span>
146
+ </div>
147
+ </div>
148
+ <div class="abilities-section">
149
+ <div class="ability-item">
150
+ <h5>Special Ability</h5>
151
+ <p>{workflowState.monsterStats.specialAbility}</p>
152
+ </div>
153
+ <div class="ability-item">
154
+ <h5>Attack</h5>
155
+ <p>{workflowState.monsterStats.attackActionDescription}</p>
156
+ </div>
157
+ <div class="ability-item">
158
+ <h5>Boost</h5>
159
+ <p>{workflowState.monsterStats.boostActionDescription}</p>
160
+ </div>
161
+ <div class="ability-item">
162
+ <h5>Disparage</h5>
163
+ <p>{workflowState.monsterStats.disparageActionDescription}</p>
164
+ </div>
165
+ <div class="ability-item">
166
+ <h5>Special</h5>
167
+ <p>{workflowState.monsterStats.specialActionDescription}</p>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ {/if}
172
  </div>
173
 
174
  <div class="action-buttons">
 
360
  to { transform: rotate(360deg); }
361
  }
362
 
363
+ .stats-grid {
364
+ display: grid;
365
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
366
+ gap: 1rem;
367
+ margin-bottom: 1.5rem;
368
+ }
369
+
370
+ .stat-item {
371
+ display: flex;
372
+ flex-direction: column;
373
+ align-items: center;
374
+ padding: 0.5rem;
375
+ background: white;
376
+ border-radius: 6px;
377
+ border: 1px solid #dee2e6;
378
+ }
379
+
380
+ .stat-label {
381
+ font-size: 0.85rem;
382
+ color: #6c757d;
383
+ margin-bottom: 0.25rem;
384
+ }
385
+
386
+ .stat-value {
387
+ font-weight: 600;
388
+ text-transform: uppercase;
389
+ font-size: 0.9rem;
390
+ }
391
+
392
+ .stat-very-low { color: #dc3545; }
393
+ .stat-low { color: #fd7e14; }
394
+ .stat-medium { color: #ffc107; }
395
+ .stat-high { color: #28a745; }
396
+ .stat-very-high { color: #007bff; }
397
+
398
+ .abilities-section {
399
+ display: grid;
400
+ gap: 1rem;
401
+ }
402
+
403
+ .ability-item {
404
+ background: white;
405
+ padding: 1rem;
406
+ border-radius: 6px;
407
+ border: 1px solid #dee2e6;
408
+ }
409
+
410
+ .ability-item h5 {
411
+ margin: 0 0 0.5rem 0;
412
+ color: #495057;
413
+ font-size: 0.95rem;
414
+ }
415
+
416
+ .ability-item p {
417
+ margin: 0;
418
+ font-size: 0.85rem;
419
+ line-height: 1.4;
420
+ }
421
+
422
  @media (max-width: 768px) {
423
  .result-container {
424
  padding: 1rem;
src/lib/components/MonsterGenerator/WorkflowProgress.svelte CHANGED
@@ -30,6 +30,11 @@
30
  label: 'Conceptualizing',
31
  description: 'Designing your monster'
32
  },
 
 
 
 
 
33
  {
34
  id: 'promptCrafting',
35
  label: 'Crafting Prompt',
 
30
  label: 'Conceptualizing',
31
  description: 'Designing your monster'
32
  },
33
+ {
34
+ id: 'statsGenerating',
35
+ label: 'Generating Stats',
36
+ description: 'Creating battle attributes'
37
+ },
38
  {
39
  id: 'promptCrafting',
40
  label: 'Crafting Prompt',
src/lib/components/MonsterGenerator/todo.txt CHANGED
@@ -1,13 +1,4 @@
1
 
2
- Long term I want to turn this into a monster battle game. As part of this I need some kind of internal DB for the game.
3
- When I worked on a Flutter-based version of this I used Isar DB, is there an equivalent that would work well here?
4
-
5
- ---
6
-
7
- How could you update the image processing so that pure white is made transparent? Could you store the transparent images in local storage?
8
-
9
- ---
10
-
11
  Note the "Sign in with Hugging Face" button that is added with my page, can I just auto
12
  <iframe src="https://fraser-piclets.static.hf.space/index.html?embed=true&amp;__sign=eyJhbGciOiJFZERTQSJ9.eyJyZWFkIjp0cnVlLCJwZXJtaXNzaW9ucyI6eyJyZXBvLmNvbnRlbnQucmVhZCI6dHJ1ZX0sIm9uQmVoYWxmT2YiOnsia2luZCI6InVzZXIiLCJfaWQiOiI1ZjE5NTc4NDkyNWI5ODYzZTI4YWQ2MTAiLCJ1c2VyIjoiRnJhc2VyIiwic2Vzc2lvbklkIjoiNjg3NjU0ZjZhYmI2ZWE2ZTk0OThkNjVmIn0sImlhdCI6MTc1MjY1NzYxMiwic3ViIjoiL3NwYWNlcy9GcmFzZXIvcGljbGV0cyIsImV4cCI6MTc1Mjc0NDAxMiwiaXNzIjoiaHR0cHM6Ly9odWdnaW5nZmFjZS5jbyJ9.vH_qEMDwpCpEapX36n-JPgfj6P7jxGdpwomhT6MIpY-r2OS9Wc1bFsQq0USfbQqKxif2rR9XL7sB8f0ximxCDA" aria-label="static space app" class="space-iframe outline-hidden grow bg-white p-0" allow="accelerometer; ambient-light-sensor; autoplay; battery; camera; clipboard-read; clipboard-write; display-capture; document-domain; encrypted-media; fullscreen; geolocation; gyroscope; layout-animations; legacy-image-formats; magnetometer; microphone; midi; oversized-images; payment; picture-in-picture; publickey-credentials-get; serial; sync-xhr; usb; vr ; wake-lock; xr-spatial-tracking" sandbox="allow-downloads allow-forms allow-modals allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts allow-storage-access-by-user-activation" scrolling="yes" id="iFrameResizer0" style="overflow: auto;"></iframe>
13
 
@@ -24,10 +15,12 @@ This will be created with the following schema:
24
  - speed: likert
25
  - special ability: string (passive trait that gives the monster a unique advantage in battle)
26
  - attack action description: string (deals damage)
27
- - attack action description: string (buff monsters own stats/status)
28
  - disparage action description: string (lowers enemy stats/status)
29
  - special action description: string (powerful action with single use per battle)
30
 
 
 
31
  ---
32
 
33
  ```python
 
1
 
 
 
 
 
 
 
 
 
 
2
  Note the "Sign in with Hugging Face" button that is added with my page, can I just auto
3
  <iframe src="https://fraser-piclets.static.hf.space/index.html?embed=true&amp;__sign=eyJhbGciOiJFZERTQSJ9.eyJyZWFkIjp0cnVlLCJwZXJtaXNzaW9ucyI6eyJyZXBvLmNvbnRlbnQucmVhZCI6dHJ1ZX0sIm9uQmVoYWxmT2YiOnsia2luZCI6InVzZXIiLCJfaWQiOiI1ZjE5NTc4NDkyNWI5ODYzZTI4YWQ2MTAiLCJ1c2VyIjoiRnJhc2VyIiwic2Vzc2lvbklkIjoiNjg3NjU0ZjZhYmI2ZWE2ZTk0OThkNjVmIn0sImlhdCI6MTc1MjY1NzYxMiwic3ViIjoiL3NwYWNlcy9GcmFzZXIvcGljbGV0cyIsImV4cCI6MTc1Mjc0NDAxMiwiaXNzIjoiaHR0cHM6Ly9odWdnaW5nZmFjZS5jbyJ9.vH_qEMDwpCpEapX36n-JPgfj6P7jxGdpwomhT6MIpY-r2OS9Wc1bFsQq0USfbQqKxif2rR9XL7sB8f0ximxCDA" aria-label="static space app" class="space-iframe outline-hidden grow bg-white p-0" allow="accelerometer; ambient-light-sensor; autoplay; battery; camera; clipboard-read; clipboard-write; display-capture; document-domain; encrypted-media; fullscreen; geolocation; gyroscope; layout-animations; legacy-image-formats; magnetometer; microphone; midi; oversized-images; payment; picture-in-picture; publickey-credentials-get; serial; sync-xhr; usb; vr ; wake-lock; xr-spatial-tracking" sandbox="allow-downloads allow-forms allow-modals allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts allow-storage-access-by-user-activation" scrolling="yes" id="iFrameResizer0" style="overflow: auto;"></iframe>
4
 
 
15
  - speed: likert
16
  - special ability: string (passive trait that gives the monster a unique advantage in battle)
17
  - attack action description: string (deals damage)
18
+ - boost action description: string (buff monsters own stats/status)
19
  - disparage action description: string (lowers enemy stats/status)
20
  - special action description: string (powerful action with single use per battle)
21
 
22
+ Also update the DB to use this JSON data in its schema.
23
+
24
  ---
25
 
26
  ```python
src/lib/components/Pages/Pictuary.svelte CHANGED
@@ -43,7 +43,7 @@
43
  {#each monsters as monster}
44
  <div class="monster-card">
45
  <img
46
- src={monster.imageUrl}
47
  alt={monster.name}
48
  class="monster-image"
49
  />
@@ -144,7 +144,11 @@
144
  .monster-image {
145
  width: 100%;
146
  aspect-ratio: 1;
147
- object-fit: cover;
 
 
 
 
148
  }
149
 
150
  .monster-name {
 
43
  {#each monsters as monster}
44
  <div class="monster-card">
45
  <img
46
+ src={monster.imageData || monster.imageUrl}
47
  alt={monster.name}
48
  class="monster-image"
49
  />
 
144
  .monster-image {
145
  width: 100%;
146
  aspect-ratio: 1;
147
+ object-fit: contain;
148
+ background: linear-gradient(45deg, #f0f0f0 25%, transparent 25%, transparent 75%, #f0f0f0 75%, #f0f0f0),
149
+ linear-gradient(45deg, #f0f0f0 25%, transparent 25%, transparent 75%, #f0f0f0 75%, #f0f0f0);
150
+ background-size: 20px 20px;
151
+ background-position: 0 0, 10px 10px;
152
  }
153
 
154
  .monster-name {
src/lib/db/index.ts CHANGED
@@ -10,6 +10,26 @@ export class MonsterDatabase extends Dexie {
10
  this.version(1).stores({
11
  monsters: '++id, name, createdAt'
12
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  }
14
  }
15
 
 
10
  this.version(1).stores({
11
  monsters: '++id, name, createdAt'
12
  });
13
+
14
+ // Version 2: Add imageData field
15
+ this.version(2).stores({
16
+ monsters: '++id, name, createdAt'
17
+ }).upgrade(tx => {
18
+ // No data migration needed, just schema update
19
+ return tx.table('monsters').toCollection().modify(monster => {
20
+ monster.imageData = monster.imageData || null;
21
+ });
22
+ });
23
+
24
+ // Version 3: Add stats field
25
+ this.version(3).stores({
26
+ monsters: '++id, name, createdAt'
27
+ }).upgrade(tx => {
28
+ // No data migration needed, just schema update
29
+ return tx.table('monsters').toCollection().modify(monster => {
30
+ monster.stats = monster.stats || null;
31
+ });
32
+ });
33
  }
34
  }
35
 
src/lib/db/schema.ts CHANGED
@@ -1,9 +1,13 @@
 
 
1
  export interface Monster {
2
  id?: number;
3
  name: string;
4
  imageUrl: string;
 
5
  imageCaption: string;
6
  concept: string;
7
  imagePrompt: string;
 
8
  createdAt: Date;
9
  }
 
1
+ import type { MonsterStats } from '$lib/types';
2
+
3
  export interface Monster {
4
  id?: number;
5
  name: string;
6
  imageUrl: string;
7
+ imageData?: string; // Base64 encoded image with transparency
8
  imageCaption: string;
9
  concept: string;
10
  imagePrompt: string;
11
+ stats?: MonsterStats; // JSON stats object
12
  createdAt: Date;
13
  }
src/lib/types/index.ts CHANGED
@@ -9,6 +9,7 @@ export interface FluxGenerationParams {
9
 
10
  export interface FluxGenerationResult {
11
  imageUrl: string;
 
12
  seed: number;
13
  prompt: string;
14
  }
@@ -77,7 +78,8 @@ export interface GradioLibs {
77
  export type MonsterWorkflowStep =
78
  | 'upload'
79
  | 'captioning'
80
- | 'conceptualizing'
 
81
  | 'promptCrafting'
82
  | 'generating'
83
  | 'complete';
@@ -87,6 +89,7 @@ export interface MonsterWorkflowState {
87
  userImage: Blob | null;
88
  imageCaption: string | null;
89
  monsterConcept: string | null;
 
90
  imagePrompt: string | null;
91
  monsterImage: FluxGenerationResult | null;
92
  error: string | null;
@@ -97,4 +100,22 @@ export interface MonsterGeneratorProps {
97
  joyCaptionClient: GradioClient | null;
98
  rwkvClient: GradioClient | null;
99
  fluxClient: GradioClient | null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  }
 
9
 
10
  export interface FluxGenerationResult {
11
  imageUrl: string;
12
+ imageData?: string; // Base64 encoded image with transparency
13
  seed: number;
14
  prompt: string;
15
  }
 
78
  export type MonsterWorkflowStep =
79
  | 'upload'
80
  | 'captioning'
81
+ | 'conceptualizing'
82
+ | 'statsGenerating'
83
  | 'promptCrafting'
84
  | 'generating'
85
  | 'complete';
 
89
  userImage: Blob | null;
90
  imageCaption: string | null;
91
  monsterConcept: string | null;
92
+ monsterStats: MonsterStats | null;
93
  imagePrompt: string | null;
94
  monsterImage: FluxGenerationResult | null;
95
  error: string | null;
 
100
  joyCaptionClient: GradioClient | null;
101
  rwkvClient: GradioClient | null;
102
  fluxClient: GradioClient | null;
103
+ }
104
+
105
+ // Monster Stats Types
106
+ export type LikertScale = 'very-low' | 'low' | 'medium' | 'high' | 'very-high';
107
+
108
+ export interface MonsterStats {
109
+ name: string;
110
+ description: string;
111
+ rarity: LikertScale;
112
+ HP: LikertScale;
113
+ defence: LikertScale;
114
+ attack: LikertScale;
115
+ speed: LikertScale;
116
+ specialAbility: string;
117
+ attackActionDescription: string;
118
+ boostActionDescription: string;
119
+ disparageActionDescription: string;
120
+ specialActionDescription: string;
121
  }
src/lib/utils/imageProcessing.ts ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Converts an image URL to a base64 data URL with white background made transparent
3
+ */
4
+ export async function makeWhiteTransparent(imageUrl: string): Promise<string> {
5
+ return new Promise((resolve, reject) => {
6
+ const img = new Image();
7
+ img.crossOrigin = 'anonymous';
8
+
9
+ img.onload = () => {
10
+ const canvas = document.createElement('canvas');
11
+ const ctx = canvas.getContext('2d');
12
+
13
+ if (!ctx) {
14
+ reject(new Error('Failed to get canvas context'));
15
+ return;
16
+ }
17
+
18
+ canvas.width = img.width;
19
+ canvas.height = img.height;
20
+
21
+ // Draw the image
22
+ ctx.drawImage(img, 0, 0);
23
+
24
+ // Get image data
25
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
26
+ const data = imageData.data;
27
+
28
+ // Define white threshold (adjust if needed)
29
+ const whiteThreshold = 240; // RGB values above this are considered "white"
30
+
31
+ // Make white pixels transparent
32
+ for (let i = 0; i < data.length; i += 4) {
33
+ const r = data[i];
34
+ const g = data[i + 1];
35
+ const b = data[i + 2];
36
+
37
+ // Check if pixel is white-ish
38
+ if (r > whiteThreshold && g > whiteThreshold && b > whiteThreshold) {
39
+ // Calculate how "white" this pixel is (0-1)
40
+ const whiteness = Math.min(r, g, b) / 255;
41
+
42
+ // Make it transparent based on whiteness
43
+ data[i + 3] = Math.floor((1 - whiteness) * 255);
44
+ }
45
+ }
46
+
47
+ // Put the modified image data back
48
+ ctx.putImageData(imageData, 0, 0);
49
+
50
+ // Convert to base64
51
+ const base64 = canvas.toDataURL('image/png');
52
+ resolve(base64);
53
+ };
54
+
55
+ img.onerror = () => {
56
+ reject(new Error('Failed to load image'));
57
+ };
58
+
59
+ img.src = imageUrl;
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Fetches an image from URL and converts it to base64
65
+ */
66
+ export async function imageUrlToBase64(imageUrl: string): Promise<string> {
67
+ return new Promise((resolve, reject) => {
68
+ const img = new Image();
69
+ img.crossOrigin = 'anonymous';
70
+
71
+ img.onload = () => {
72
+ const canvas = document.createElement('canvas');
73
+ const ctx = canvas.getContext('2d');
74
+
75
+ if (!ctx) {
76
+ reject(new Error('Failed to get canvas context'));
77
+ return;
78
+ }
79
+
80
+ canvas.width = img.width;
81
+ canvas.height = img.height;
82
+ ctx.drawImage(img, 0, 0);
83
+
84
+ const base64 = canvas.toDataURL('image/png');
85
+ resolve(base64);
86
+ };
87
+
88
+ img.onerror = () => {
89
+ reject(new Error('Failed to load image'));
90
+ };
91
+
92
+ img.src = imageUrl;
93
+ });
94
+ }