@zodic/shared 0.0.357 → 0.0.359

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,631 @@
1
+ import { and, eq } from 'drizzle-orm';
2
+ import { inject, injectable } from 'inversify';
3
+ import { v4 } from 'uuid';
4
+ import { ImageDescriberService } from './ImageDescriberService';
5
+ import { AppContext } from '../base';
6
+ import { LeonardoService } from './LeonardoService';
7
+ import { ArtifactList, schema } from '../..';
8
+ import { sizes } from '../../types/scopes/legacy';
9
+ import { users } from '../../db/schema';
10
+
11
+ @injectable()
12
+ export class ArtifactService {
13
+ constructor(
14
+ @inject(AppContext) private context: AppContext,
15
+ @inject(LeonardoService) private leonardoService: LeonardoService,
16
+ @inject(ImageDescriberService)
17
+ private imageDescriberService: ImageDescriberService
18
+ ) {}
19
+
20
+ private async log(
21
+ level: 'info' | 'debug' | 'warn' | 'error',
22
+ message: string,
23
+ context: Record<string, any> = {}
24
+ ) {
25
+ const logId = `artifact-service:${Date.now()}`;
26
+ const logMessage = `[${level.toUpperCase()}] ${message}`;
27
+
28
+ console[level](logMessage, context);
29
+ const db = this.context.drizzle();
30
+ try {
31
+ await db
32
+ .insert(schema.logs)
33
+ .values({
34
+ id: logId,
35
+ level,
36
+ message,
37
+ context: JSON.stringify(context),
38
+ createdAt: new Date().getTime(),
39
+ })
40
+ .execute();
41
+ } catch (error) {
42
+ console.error('[ERROR] Failed to persist log to database:', {
43
+ error,
44
+ logId,
45
+ message,
46
+ rawContext: context,
47
+ stringifiedContext: JSON.stringify(context),
48
+ createdAt: new Date().getTime(),
49
+ });
50
+ }
51
+ }
52
+
53
+ async generateCosmicMirror(
54
+ artifactId: string,
55
+ userId: string,
56
+ generatedImageId: string,
57
+ archetypeDataId: string
58
+ ) {
59
+ const { height, width } = sizes['alchemy']['post4:5'];
60
+
61
+ await this.log('info', 'Starting cosmic mirror generation', {
62
+ artifactId,
63
+ userId,
64
+ generatedImageId,
65
+ archetypeDataId,
66
+ });
67
+
68
+ const drizzle = this.context.drizzle();
69
+ const {
70
+ artifacts,
71
+ userArtifacts,
72
+ generations,
73
+ userConcepts,
74
+ cosmicMirrorImages,
75
+ } = schema;
76
+
77
+ try {
78
+ // Fetch the artifact and its linked conceptId first
79
+ const [artifact] = await drizzle
80
+ .select({
81
+ id: artifacts.id,
82
+ conceptId: artifacts.conceptId,
83
+ })
84
+ .from(artifacts)
85
+ .where(eq(artifacts.slug, ArtifactList.COSMIC_MIRROR))
86
+ .limit(1);
87
+
88
+ if (!artifact || !artifact.id || !artifact.conceptId) {
89
+ return {
90
+ status: 'error',
91
+ error: 'Artifact or linked concept not found. Check registration.',
92
+ statusCode: 500,
93
+ };
94
+ }
95
+
96
+ // Check for existing cosmic mirror artifact
97
+ const [existingArtifact] = await drizzle
98
+ .select()
99
+ .from(userArtifacts)
100
+ .where(
101
+ and(
102
+ eq(userArtifacts.userId, userId),
103
+ eq(userArtifacts.artifactId, artifact.id)
104
+ )
105
+ )
106
+ .limit(1);
107
+
108
+ if (existingArtifact) {
109
+ const artifactAge =
110
+ Date.now() - new Date(existingArtifact.createdAt).getTime();
111
+ const twentyMinutesMs = 20 * 60 * 1000;
112
+
113
+ if (
114
+ existingArtifact.status === 'pending' &&
115
+ artifactAge < twentyMinutesMs
116
+ ) {
117
+ return {
118
+ status: 'error',
119
+ error:
120
+ 'You already have a cosmic mirror being generated. Please wait for it to complete.',
121
+ statusCode: 400,
122
+ };
123
+ }
124
+
125
+ // Check D1 for existing images
126
+ const existingImages = await drizzle
127
+ .select()
128
+ .from(cosmicMirrorImages)
129
+ .where(eq(cosmicMirrorImages.userArtifactId, existingArtifact.id));
130
+
131
+ if (existingImages.length >= 6 && artifactAge >= twentyMinutesMs) {
132
+ // If we have 6 images and artifact is old, trigger faceswaps
133
+ return await this.retryStaleCosmicMirror(existingArtifact.id);
134
+ }
135
+
136
+ // Clean up old artifact if incomplete
137
+ if (artifactAge >= twentyMinutesMs) {
138
+ await this.cleanupStaleCosmicMirror(existingArtifact.id);
139
+ } else {
140
+ return {
141
+ status: 'error',
142
+ error: 'You can only have one cosmic mirror artifact at this time.',
143
+ statusCode: 400,
144
+ };
145
+ }
146
+ }
147
+
148
+ // Fetch user information
149
+ const [user] = await drizzle
150
+ .select({
151
+ userPhotoId: users.userPhotoId,
152
+ userPhotoUrl: users.userPhotoUrl,
153
+ userGender: users.gender,
154
+ profileImage: users.profileImage,
155
+ })
156
+ .from(users)
157
+ .where(eq(users.id, userId))
158
+ .limit(1);
159
+
160
+ if (!user) {
161
+ return {
162
+ status: 'error',
163
+ error: 'User not found',
164
+ statusCode: 400,
165
+ };
166
+ }
167
+
168
+ let userPhotoId = user.userPhotoId;
169
+ let userPhotoUrl = user.userPhotoUrl;
170
+
171
+ // Upload profileImage to Leonardo if userPhotoId or userPhotoUrl is missing
172
+ if (!userPhotoId || !userPhotoUrl) {
173
+ if (!user.profileImage) {
174
+ return {
175
+ status: 'error',
176
+ error:
177
+ 'User profile image not found. Please upload an image first.',
178
+ statusCode: 400,
179
+ };
180
+ }
181
+
182
+ console.info(
183
+ `Uploading user profile image to Leonardo: ${user.profileImage}`
184
+ );
185
+
186
+ const uploadResponse = await this.leonardoService.uploadImageByUrl(
187
+ user.profileImage
188
+ );
189
+
190
+ if (!uploadResponse) {
191
+ return {
192
+ status: 'error',
193
+ error: 'Failed to upload user profile image to Leonardo',
194
+ statusCode: 500,
195
+ };
196
+ }
197
+
198
+ userPhotoId = uploadResponse.uploadedImageId;
199
+ userPhotoUrl = uploadResponse.uploadedImageUrl;
200
+
201
+ console.info('User profile image uploaded to Leonardo:', {
202
+ userPhotoId,
203
+ userPhotoUrl,
204
+ });
205
+
206
+ // Update user record with new photo data
207
+ await drizzle
208
+ .update(users)
209
+ .set({
210
+ userPhotoId,
211
+ userPhotoUrl,
212
+ updatedAt: new Date(),
213
+ })
214
+ .where(eq(users.id, userId));
215
+ }
216
+
217
+ if (!user.userGender) {
218
+ return {
219
+ status: 'error',
220
+ error: 'User gender not found. Please define a gender first.',
221
+ statusCode: 400,
222
+ };
223
+ }
224
+
225
+ // Fetch the user's concept with combinationId
226
+ const [userConcept] = await drizzle
227
+ .select({
228
+ id: userConcepts.id,
229
+ conceptCombinationId: userConcepts.conceptCombinationId,
230
+ })
231
+ .from(userConcepts)
232
+ .where(
233
+ and(
234
+ eq(userConcepts.userId, userId),
235
+ eq(userConcepts.conceptId, artifact.conceptId)
236
+ )
237
+ )
238
+ .limit(1);
239
+
240
+ if (!userConcept) {
241
+ return {
242
+ status: 'error',
243
+ error: 'User concept not found. Please generate the concept first.',
244
+ statusCode: 400,
245
+ };
246
+ }
247
+
248
+ // Get the archetype data record
249
+ const [archetypeData] = await drizzle
250
+ .select({
251
+ id: schema.archetypesData.id,
252
+ leonardoPrompt: schema.archetypesData.leonardoPrompt,
253
+ })
254
+ .from(schema.archetypesData)
255
+ .where(eq(schema.archetypesData.id, archetypeDataId))
256
+ .limit(1);
257
+
258
+ if (!archetypeData) {
259
+ return {
260
+ status: 'error',
261
+ error: 'Archetype data not found for this combination and gender.',
262
+ statusCode: 404,
263
+ };
264
+ }
265
+
266
+ if (!archetypeData.leonardoPrompt) {
267
+ return {
268
+ status: 'error',
269
+ error: `Archetype data is incomplete. Leonardo Prompt not found for ArchetypeDataId: ${archetypeData}`,
270
+ statusCode: 404,
271
+ };
272
+ }
273
+
274
+ // Create the user artifact with new schema
275
+ const userArtifactId = v4();
276
+ await drizzle.insert(userArtifacts).values({
277
+ id: userArtifactId,
278
+ userId,
279
+ artifactId: artifact.id,
280
+ userConceptId: userConcept.id,
281
+ conceptId: artifact.conceptId,
282
+ archetypeDataId: archetypeData.id,
283
+ postImages: '[]',
284
+ status: 'pending',
285
+ createdAt: new Date(),
286
+ });
287
+
288
+ // Describe the user photo
289
+ const traits = await this.imageDescriberService.describeImage({
290
+ imageUrl: userPhotoUrl,
291
+ });
292
+
293
+ // Personalize the Leonardo prompt
294
+ const personalizedPrompt =
295
+ await this.leonardoService.personalizeCosmicMirrorLeonardoPrompt({
296
+ leonardoPrompt: archetypeData.leonardoPrompt,
297
+ traits,
298
+ });
299
+
300
+ // First generation - with both control nets (content + character)
301
+ const generationResponse1 = await this.context
302
+ .api()
303
+ .callLeonardo.generateImage({
304
+ prompt: personalizedPrompt,
305
+ width,
306
+ height,
307
+ quantity: 3,
308
+ controlNets: [
309
+ {
310
+ initImageId: userPhotoId,
311
+ initImageType: 'UPLOADED',
312
+ preprocessorId: 133, // Character Reference
313
+ strengthType: 'High',
314
+ },
315
+ {
316
+ initImageId: generatedImageId,
317
+ initImageType: 'GENERATED',
318
+ preprocessorId: 100, // Content Reference
319
+ strengthType: 'Low',
320
+ },
321
+ ],
322
+ });
323
+
324
+ if (
325
+ !generationResponse1 ||
326
+ !generationResponse1.sdGenerationJob ||
327
+ !generationResponse1.sdGenerationJob.generationId
328
+ ) {
329
+ console.error('First Leonardo generation response is incorrect.');
330
+ return {
331
+ status: 'error',
332
+ error: 'First Leonardo generation failed.',
333
+ statusCode: 500,
334
+ };
335
+ }
336
+
337
+ const generationId1 = generationResponse1.sdGenerationJob.generationId;
338
+ await drizzle.insert(generations).values({
339
+ id: generationId1,
340
+ userId,
341
+ artifactId: artifact.id,
342
+ userArtifactId,
343
+ type: ArtifactList.COSMIC_MIRROR,
344
+ status: 'pending',
345
+ createdAt: new Date(),
346
+ });
347
+
348
+ // Second generation - with only character control net
349
+ const generationResponse2 = await this.context
350
+ .api()
351
+ .callLeonardo.generateImage({
352
+ prompt: personalizedPrompt,
353
+ width,
354
+ height,
355
+ quantity: 3,
356
+ controlNets: [
357
+ {
358
+ initImageId: userPhotoId,
359
+ initImageType: 'UPLOADED',
360
+ preprocessorId: 133, // Character Reference
361
+ strengthType: 'High',
362
+ },
363
+ ],
364
+ });
365
+
366
+ if (
367
+ !generationResponse2 ||
368
+ !generationResponse2.sdGenerationJob ||
369
+ !generationResponse2.sdGenerationJob.generationId
370
+ ) {
371
+ console.error('Second Leonardo generation response is incorrect.');
372
+ return {
373
+ status: 'error',
374
+ error: 'Second Leonardo generation failed.',
375
+ statusCode: 500,
376
+ };
377
+ }
378
+
379
+ const generationId2 = generationResponse2.sdGenerationJob.generationId;
380
+ await drizzle.insert(generations).values({
381
+ id: generationId2,
382
+ userId,
383
+ artifactId: artifact.id,
384
+ userArtifactId,
385
+ type: ArtifactList.COSMIC_MIRROR,
386
+ status: 'pending',
387
+ createdAt: new Date(),
388
+ });
389
+
390
+ return {
391
+ status: 'success',
392
+ data: {
393
+ generations: {
394
+ contentCharacter: generationResponse1,
395
+ characterOnly: generationResponse2,
396
+ },
397
+ userArtifactId,
398
+ },
399
+ statusCode: 200,
400
+ };
401
+ } catch (error) {
402
+ console.error('Error generating user artifact:', error);
403
+ return {
404
+ status: 'error',
405
+ error: 'Failed to generate user artifact',
406
+ statusCode: 500,
407
+ };
408
+ }
409
+ }
410
+
411
+ async cleanupStaleCosmicMirror(userArtifactId: string) {
412
+ const drizzle = this.context.drizzle();
413
+ const { cosmicMirrorImages, generations, userArtifacts } = schema;
414
+
415
+ // Delete images from cosmicMirrorImages
416
+ await drizzle
417
+ .delete(cosmicMirrorImages)
418
+ .where(eq(cosmicMirrorImages.userArtifactId, userArtifactId));
419
+
420
+ // Delete associated generations
421
+ await drizzle
422
+ .delete(generations)
423
+ .where(eq(generations.userArtifactId, userArtifactId));
424
+
425
+ // Delete the user artifact
426
+ await drizzle
427
+ .delete(userArtifacts)
428
+ .where(eq(userArtifacts.id, userArtifactId));
429
+
430
+ await this.log('info', 'Cleaned up stale cosmic mirror artifact', {
431
+ userArtifactId,
432
+ });
433
+ }
434
+
435
+ async retryStaleCosmicMirror(userArtifactId: string): Promise<any> {
436
+ const drizzle = this.context.drizzle();
437
+ const { users, artifactFaceswap, cosmicMirrorImages, userArtifacts } =
438
+ schema;
439
+
440
+ console.log(
441
+ `Starting retryStaleCosmicMirror for userArtifactId: ${userArtifactId}`
442
+ );
443
+
444
+ // Fetch userId from userArtifacts
445
+ console.log('Fetching user artifact to get userId...');
446
+ const [artifact] = await drizzle
447
+ .select({ userId: userArtifacts.userId })
448
+ .from(userArtifacts)
449
+ .where(eq(userArtifacts.id, userArtifactId))
450
+ .limit(1);
451
+
452
+ if (!artifact || !artifact.userId) {
453
+ console.error('User artifact or userId not found');
454
+ return {
455
+ status: 'error',
456
+ error: 'User artifact or userId not found',
457
+ statusCode: 400,
458
+ };
459
+ }
460
+
461
+ const userId = artifact.userId;
462
+ console.log(`Fetched userId: ${userId}`);
463
+
464
+ // Fetch user photo
465
+ console.log('Fetching user photo from database...');
466
+ const [user] = await drizzle
467
+ .select({
468
+ userPhotoUrl: users.userPhotoUrl,
469
+ userProfileImage: users.profileImage,
470
+ })
471
+ .from(users)
472
+ .where(eq(users.id, userId))
473
+ .limit(1);
474
+
475
+ console.log('User fetch result:', user);
476
+
477
+ const sourceImage = user?.userPhotoUrl || user?.userProfileImage;
478
+ if (!sourceImage) {
479
+ console.error('No user photo found for faceswap');
480
+ return {
481
+ status: 'error',
482
+ error: 'No user photo found for faceswap',
483
+ statusCode: 400,
484
+ };
485
+ }
486
+ console.log(`Source image URL: ${sourceImage}`);
487
+
488
+ // Get all images from D1
489
+ console.log('Fetching images from D1 cosmicMirrorImages table...');
490
+ const images = await drizzle
491
+ .select()
492
+ .from(cosmicMirrorImages)
493
+ .where(eq(cosmicMirrorImages.userArtifactId, userArtifactId));
494
+
495
+ console.log('Images retrieved from D1:', images);
496
+
497
+ if (images.length === 0) {
498
+ console.warn('No images found in D1 for this userArtifactId');
499
+ return {
500
+ status: 'error',
501
+ error: 'No images found in D1 for faceswap',
502
+ statusCode: 400,
503
+ };
504
+ }
505
+
506
+ // Process faceswaps for all images
507
+ let faceswapCount = 0;
508
+ for (const image of images) {
509
+ console.log(`Processing image with leonardoId: ${image.leonardoId}`);
510
+
511
+ if (!image.url) {
512
+ console.warn(
513
+ `Image with leonardoId ${image.leonardoId} does not contain a URL:`,
514
+ image
515
+ );
516
+ continue;
517
+ }
518
+ console.log(
519
+ `Target image URL for leonardoId ${image.leonardoId}: ${image.url}`
520
+ );
521
+
522
+ console.log(
523
+ `Calling faceSwap API for source: ${sourceImage}, target: ${image.url}`
524
+ );
525
+ let faceSwapResponse: any;
526
+ try {
527
+ // Add a timeout for the FaceSwap API call (8 seconds as specified)
528
+ const timeoutPromise = new Promise((_, reject) => {
529
+ setTimeout(
530
+ () =>
531
+ reject(new Error('FaceSwap API call timed out after 8 seconds')),
532
+ 8000
533
+ );
534
+ });
535
+
536
+ faceSwapResponse = await Promise.race([
537
+ this.context.api().callPiApi.faceSwap({
538
+ sourceImageUrl: sourceImage,
539
+ targetImageUrl: image.url,
540
+ }),
541
+ timeoutPromise,
542
+ ]);
543
+
544
+ console.log(
545
+ `faceSwap response for leonardoId ${image.leonardoId}:`,
546
+ faceSwapResponse
547
+ );
548
+ } catch (err) {
549
+ console.error(
550
+ `faceSwap API call failed for leonardoId ${image.leonardoId}:`,
551
+ err
552
+ );
553
+ continue;
554
+ }
555
+
556
+ if (!faceSwapResponse || !faceSwapResponse.taskId) {
557
+ console.error(
558
+ `faceSwap response for leonardoId ${image.leonardoId} does not contain a taskId:`,
559
+ faceSwapResponse
560
+ );
561
+ continue;
562
+ }
563
+
564
+ // Create faceswap record
565
+ console.log(
566
+ `Creating artifactFaceswap record for taskId: ${faceSwapResponse.taskId}`
567
+ );
568
+ try {
569
+ await drizzle.insert(artifactFaceswap).values({
570
+ id: faceSwapResponse.taskId,
571
+ userArtifactId,
572
+ status: 'pending',
573
+ createdAt: new Date(),
574
+ updatedAt: new Date(),
575
+ });
576
+ console.log(
577
+ `Successfully created artifactFaceswap record for taskId: ${faceSwapResponse.taskId}`
578
+ );
579
+ faceswapCount++;
580
+ } catch (err) {
581
+ console.error(
582
+ `Failed to create artifactFaceswap record for taskId: ${faceSwapResponse.taskId}:`,
583
+ err
584
+ );
585
+ continue;
586
+ }
587
+ }
588
+
589
+ if (faceswapCount === 0) {
590
+ console.error('No artifactFaceswap records were created');
591
+ return {
592
+ status: 'error',
593
+ error: 'Failed to create any faceswap records',
594
+ statusCode: 500,
595
+ };
596
+ }
597
+
598
+ // Update user artifact status
599
+ console.log(
600
+ `Updating userArtifacts status to 'faceswap' for userArtifactId: ${userArtifactId}`
601
+ );
602
+ try {
603
+ await drizzle
604
+ .update(userArtifacts)
605
+ .set({
606
+ status: 'faceswap',
607
+ updatedAt: new Date(),
608
+ })
609
+ .where(eq(userArtifacts.id, userArtifactId));
610
+ console.log('Successfully updated userArtifacts status');
611
+ } catch (err) {
612
+ console.error('Failed to update userArtifacts status:', err);
613
+ return {
614
+ status: 'error',
615
+ error: 'Failed to update user artifact status',
616
+ statusCode: 500,
617
+ };
618
+ }
619
+
620
+ console.log(
621
+ `retryStaleCosmicMirror completed successfully. Created ${faceswapCount} faceswap records.`
622
+ );
623
+ return {
624
+ status: 'success',
625
+ data: {
626
+ message: 'Restarted faceswap process for existing cosmic mirror images',
627
+ },
628
+ statusCode: 200,
629
+ };
630
+ }
631
+ }
@@ -0,0 +1,45 @@
1
+ import { inject, injectable } from 'inversify';
2
+ import 'reflect-metadata';
3
+ import { AppContext } from '../base';
4
+
5
+ @injectable()
6
+ export class ImageDescriberService {
7
+ constructor(@inject(AppContext) private context: AppContext) {}
8
+
9
+ async describeImage({ imageUrl }: { imageUrl: string }): Promise<string> {
10
+ try {
11
+ const response = await this.context.api().callTogether.single(
12
+ [
13
+ {
14
+ role: 'user',
15
+ content: [
16
+ {
17
+ type: 'text',
18
+ text: 'Describe the following traits of the person in the picture:\n- Hair color, size and style\n- Skin color\n- Eye color\n\nReturn the result in the following format:\n\nHair: hair description\nSkin: skin description\nEye: eye color',
19
+ },
20
+ {
21
+ type: 'image_url',
22
+ image_url: {
23
+ url: imageUrl,
24
+ },
25
+ },
26
+ ],
27
+ },
28
+ ],
29
+ {
30
+ model: 'meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo',
31
+ temperature: 0.3,
32
+ }
33
+ );
34
+
35
+ if (!response) {
36
+ throw new Error('Failed to generate image description');
37
+ }
38
+
39
+ return response;
40
+ } catch (error) {
41
+ console.error('Error describing image:', error);
42
+ throw error;
43
+ }
44
+ }
45
+ }
@@ -1,3 +1,5 @@
1
1
  export * from './ArchetypeService';
2
+ export * from './ArtifactService';
2
3
  export * from './ConceptService';
4
+ export * from './ImageDescriberService';
3
5
  export * from './LeonardoService';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zodic/shared",
3
- "version": "0.0.357",
3
+ "version": "0.0.359",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -40,6 +40,7 @@ export type CentralBindings = {
40
40
  LANTERN_QUEUE: Queue;
41
41
  ORB_QUEUE: Queue;
42
42
  ARCHETYPE_POPULATION_QUEUE: Queue;
43
+ FACESWAP_QUEUE: Queue;
43
44
 
44
45
  PROMPT_IMAGE_DESCRIBER: string;
45
46
  PROMPT_GENERATE_ARCHETYPE_BASIC_INFO: string;
@@ -143,6 +144,7 @@ export type BackendBindings = Env &
143
144
  | 'KV_API_USAGE'
144
145
  | 'KV_CONCEPT_CACHE'
145
146
  | 'ENV'
147
+ | 'FACESWAP_QUEUE'
146
148
  >;
147
149
 
148
150
  export type BackendCtx = Context<