@umituz/react-native-ai-generation-content 1.83.75 → 1.83.77

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.83.75",
3
+ "version": "1.83.77",
4
4
  "description": "Provider-agnostic AI generation orchestration for React Native with result preview components",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
package/src/core/index.ts CHANGED
@@ -52,6 +52,8 @@ export type {
52
52
  VideoFeatureInputData,
53
53
  // Main Provider Interface
54
54
  IAIProvider,
55
+ // Logging
56
+ ProviderLogEntry,
55
57
  } from "../domain/interfaces/ai-provider.interface";
56
58
 
57
59
  // Generation Constants
@@ -42,6 +42,9 @@ export type {
42
42
  VideoFeatureInputData,
43
43
  } from "./ai-provider-input.types";
44
44
 
45
+ // Lifecycle & Logging
46
+ export type { ProviderLogEntry } from "./provider-lifecycle.interface";
47
+
45
48
  // Main Interface Composition (Interface Segregation Principle)
46
49
  import type { IAIProviderLifecycle } from "./provider-lifecycle.interface";
47
50
  import type { IAIProviderCapabilities } from "./provider-capabilities.interface";
@@ -5,6 +5,16 @@
5
5
 
6
6
  import type { AIProviderConfig } from "./ai-provider.interface";
7
7
 
8
+ /** Log entry from generation session */
9
+ export interface ProviderLogEntry {
10
+ readonly timestamp: number;
11
+ readonly elapsed: number;
12
+ readonly level: 'info' | 'warn' | 'error';
13
+ readonly tag: string;
14
+ readonly message: string;
15
+ readonly data?: Record<string, unknown>;
16
+ }
17
+
8
18
  export interface IAIProviderLifecycle {
9
19
  /**
10
20
  * Initialize provider with configuration
@@ -20,4 +30,15 @@ export interface IAIProviderLifecycle {
20
30
  * Reset provider state to uninitialized
21
31
  */
22
32
  reset(): void;
33
+
34
+ /**
35
+ * Get log entries from the current/last generation session.
36
+ * Returns empty array if not supported.
37
+ */
38
+ getSessionLogs?(): ProviderLogEntry[];
39
+
40
+ /**
41
+ * End log session and return all entries. Clears the buffer.
42
+ */
43
+ endLogSession?(): ProviderLogEntry[];
23
44
  }
@@ -12,6 +12,7 @@ import type {
12
12
  import type { GenerationResult } from "../../../../domain/entities/generation.types";
13
13
  import { providerRegistry } from "../../../../infrastructure/services/provider-registry.service";
14
14
  import { env } from "../../../../infrastructure/config/env.config";
15
+ import { addGenerationLogs, addGenerationLog, startGenerationLogSession } from "../../../../infrastructure/services/generation-log-store";
15
16
 
16
17
 
17
18
  export class ImageExecutor
@@ -22,46 +23,40 @@ export class ImageExecutor
22
23
  input: ImageGenerationInput,
23
24
  options?: GenerationOptions,
24
25
  ): Promise<GenerationResult<ImageGenerationOutput>> {
26
+ const TAG = 'GenericImageExecutor';
27
+ const startTime = Date.now();
28
+
29
+ startGenerationLogSession();
30
+
25
31
  try {
26
- if (typeof __DEV__ !== "undefined" && __DEV__) {
27
- console.log("[ImageExecutor] Starting generation", {
28
- model,
29
- hasImages: !!input.imageUrls,
30
- imageCount: input.imageUrls?.length || 0,
31
- promptLength: input.prompt?.length || 0,
32
- });
33
- }
32
+ const totalImageSize = input.imageUrls?.reduce((sum, url) => sum + url.length, 0) ?? 0;
33
+ addGenerationLog(TAG, 'Starting generation', 'info', {
34
+ model,
35
+ imageCount: input.imageUrls?.length || 0,
36
+ totalImageSizeKB: Math.round(totalImageSize / 1024),
37
+ promptLength: input.prompt?.length || 0,
38
+ });
34
39
 
35
40
  const provider = providerRegistry.getActiveProvider();
36
41
 
37
42
  if (!provider?.isInitialized()) {
38
- if (typeof __DEV__ !== "undefined" && __DEV__) {
39
- console.error("[ImageExecutor] Provider not initialized");
40
- }
43
+ addGenerationLog(TAG, 'Provider not initialized!', 'error');
41
44
  return { success: false, error: "AI provider not initialized" };
42
45
  }
43
46
 
44
47
  options?.onProgress?.(5);
45
-
46
48
  const modelInput = this.buildModelInput(input);
47
-
48
- if (typeof __DEV__ !== "undefined" && __DEV__) {
49
- console.log("[ImageExecutor] Model input prepared", {
50
- imageCount: modelInput.image_urls?.length || 0,
51
- aspectRatio: modelInput.aspect_ratio,
52
- outputFormat: modelInput.output_format,
53
- });
54
- }
49
+ addGenerationLog(TAG, 'Model input prepared', 'info', {
50
+ imageCount: modelInput.image_urls?.length || 0,
51
+ aspectRatio: modelInput.aspect_ratio,
52
+ });
55
53
 
56
54
  options?.onProgress?.(10);
55
+ addGenerationLog(TAG, 'Calling provider.subscribe()...');
57
56
 
58
57
  const result = await provider.subscribe(model, modelInput, {
59
58
  timeoutMs: options?.timeoutMs ?? env.generationImageTimeoutMs,
60
59
  onQueueUpdate: (status) => {
61
- if (typeof __DEV__ !== "undefined" && __DEV__) {
62
- console.log("[ImageExecutor] Queue status:", status.status);
63
- }
64
-
65
60
  if (status.status === "IN_QUEUE") {
66
61
  options?.onProgress?.(20);
67
62
  } else if (status.status === "IN_PROGRESS") {
@@ -70,37 +65,33 @@ export class ImageExecutor
70
65
  },
71
66
  });
72
67
 
73
- options?.onProgress?.(90);
68
+ // Collect provider logs
69
+ const providerLogs = provider.endLogSession?.() ?? provider.getSessionLogs?.() ?? [];
70
+ addGenerationLogs(providerLogs);
74
71
 
72
+ options?.onProgress?.(90);
75
73
  const imageUrl = this.extractImageUrl(result);
74
+ const elapsed = Date.now() - startTime;
76
75
 
77
76
  if (!imageUrl) {
78
- if (typeof __DEV__ !== "undefined" && __DEV__) {
79
- console.error("[ImageExecutor] No image URL in response");
80
- }
77
+ addGenerationLog(TAG, `No image URL in response after ${elapsed}ms`, 'error');
81
78
  return { success: false, error: "No image generated" };
82
79
  }
83
80
 
84
81
  options?.onProgress?.(100);
85
-
86
- if (typeof __DEV__ !== "undefined" && __DEV__) {
87
- console.log("[ImageExecutor] Generation successful", {
88
- imageUrl: imageUrl.slice(0, 100),
89
- });
90
- }
91
-
82
+ addGenerationLog(TAG, `Generation SUCCESS in ${elapsed}ms`, 'info', { imageUrl, elapsed });
92
83
  return { success: true, data: { imageUrl } };
93
84
  } catch (error) {
94
- const message =
95
- error instanceof Error ? error.message : "Generation failed";
96
-
97
- if (typeof __DEV__ !== "undefined" && __DEV__) {
98
- console.error("[ImageExecutor] Generation error:", {
99
- message,
100
- error,
101
- });
85
+ // Collect provider logs even on failure
86
+ const provider = providerRegistry.getActiveProvider();
87
+ if (provider) {
88
+ const providerLogs = provider.endLogSession?.() ?? provider.getSessionLogs?.() ?? [];
89
+ addGenerationLogs(providerLogs);
102
90
  }
103
91
 
92
+ const elapsed = Date.now() - startTime;
93
+ const message = error instanceof Error ? error.message : "Generation failed";
94
+ addGenerationLog(TAG, `Generation FAILED after ${elapsed}ms: ${message}`, 'error', { elapsed });
104
95
  return { success: false, error: message };
105
96
  }
106
97
  }
@@ -11,6 +11,7 @@ import {
11
11
  DEFAULT_STYLE_VALUE,
12
12
  MODEL_INPUT_DEFAULTS,
13
13
  } from "./wizard-strategy.constants";
14
+ import { addGenerationLogs, addGenerationLog, startGenerationLogSession } from "../../../../../infrastructure/services/generation-log-store";
14
15
 
15
16
 
16
17
  interface ExecutionResult {
@@ -51,22 +52,31 @@ export async function executeImageGeneration(
51
52
  model: string,
52
53
  onProgress?: (progress: number) => void,
53
54
  ): Promise<ExecutionResult> {
55
+ const TAG = 'ImageExecutor';
56
+ const startTime = Date.now();
57
+ startGenerationLogSession();
54
58
  const { providerRegistry } = await import("../../../../../infrastructure/services/provider-registry.service");
55
59
 
56
60
  const provider = providerRegistry.getActiveProvider();
57
61
  if (!provider?.isInitialized()) {
62
+ addGenerationLog(TAG, 'Provider not initialized!', 'error');
58
63
  return { success: false, error: "AI provider not initialized" };
59
64
  }
60
65
 
61
66
  try {
62
67
  const imageUrls = input.photos.map(formatBase64);
63
68
  const finalPrompt = buildFinalPrompt(input, imageUrls);
69
+ const mode = imageUrls.length > 0 ? "Photo-based" : "Text-to-image";
70
+ const totalImageSize = imageUrls.reduce((sum, url) => sum + url.length, 0);
64
71
 
65
- if (typeof __DEV__ !== "undefined" && __DEV__) {
66
- const mode = imageUrls.length > 0 ? "Photo-based" : "Text-to-image";
67
- console.log(`[ImageExecutor] ${mode} generation`, { personCount: imageUrls.length });
68
- console.log(`[ImageExecutor] Final prompt (${finalPrompt.length} chars):\n${finalPrompt.substring(0, 800)}${finalPrompt.length > 800 ? "\n...[truncated]" : ""}`);
69
- }
72
+ addGenerationLog(TAG, `${mode} generation starting`, 'info', {
73
+ model,
74
+ photoCount: imageUrls.length,
75
+ totalImageSizeKB: Math.round(totalImageSize / 1024),
76
+ promptLength: finalPrompt.length,
77
+ timeout: GENERATION_TIMEOUT_MS,
78
+ });
79
+ addGenerationLog(TAG, `Prompt: ${finalPrompt.substring(0, 300)}${finalPrompt.length > 300 ? "..." : ""}`);
70
80
 
71
81
  const modelInput: Record<string, unknown> = {
72
82
  prompt: finalPrompt,
@@ -82,21 +92,45 @@ export async function executeImageGeneration(
82
92
  }
83
93
 
84
94
  let lastStatus = "";
95
+ addGenerationLog(TAG, 'Calling provider.subscribe()...');
85
96
  const result = await provider.subscribe(model, modelInput, {
86
97
  timeoutMs: GENERATION_TIMEOUT_MS,
87
98
  onQueueUpdate: (status) => {
88
- if (status.status !== lastStatus) lastStatus = status.status;
99
+ if (status.status !== lastStatus) {
100
+ lastStatus = status.status;
101
+ }
89
102
  },
90
103
  });
91
104
 
105
+ // Collect provider logs after subscribe completes
106
+ const providerLogs = provider.endLogSession?.() ?? provider.getSessionLogs?.() ?? [];
107
+ addGenerationLogs(providerLogs);
108
+
92
109
  const rawResult = result as Record<string, unknown>;
93
110
  const data = (rawResult?.data ?? rawResult) as { images?: Array<{ url: string }> };
94
111
  const imageUrl = data?.images?.[0]?.url;
95
112
 
113
+ const elapsed = Date.now() - startTime;
96
114
  onProgress?.(100);
97
115
 
98
- return imageUrl ? { success: true, imageUrl } : { success: false, error: "No image generated" };
116
+ if (imageUrl) {
117
+ addGenerationLog(TAG, `Generation SUCCESS in ${elapsed}ms`, 'info', { imageUrl, elapsed });
118
+ return { success: true, imageUrl };
119
+ }
120
+
121
+ addGenerationLog(TAG, `No image in response after ${elapsed}ms`, 'error', {
122
+ responseKeys: Object.keys(data || {}),
123
+ elapsed,
124
+ });
125
+ return { success: false, error: "No image generated" };
99
126
  } catch (error) {
100
- return { success: false, error: error instanceof Error ? error.message : "Generation failed" };
127
+ // Collect provider logs even on failure
128
+ const providerLogs = provider.endLogSession?.() ?? provider.getSessionLogs?.() ?? [];
129
+ addGenerationLogs(providerLogs);
130
+
131
+ const elapsed = Date.now() - startTime;
132
+ const errorMsg = error instanceof Error ? error.message : "Generation failed";
133
+ addGenerationLog(TAG, `Generation FAILED after ${elapsed}ms: ${errorMsg}`, 'error', { elapsed });
134
+ return { success: false, error: errorMsg };
101
135
  }
102
136
  }
@@ -1,6 +1,11 @@
1
1
  /**
2
2
  * Photo Extraction Utilities
3
3
  * Shared photo extraction logic for wizard strategies
4
+ *
5
+ * Resize strategy:
6
+ * - Small images (<300px): scale UP to 300px minimum (AI provider requirement)
7
+ * - Large images (>1536px): scale DOWN to 1536px maximum (reduces upload size ~10x)
8
+ * - Normal images: pass through unchanged
4
9
  */
5
10
 
6
11
  import { readFileAsBase64 } from "@umituz/react-native-design-system/filesystem";
@@ -10,6 +15,7 @@ import { PHOTO_KEY_PREFIX } from "../wizard-strategy.constants";
10
15
 
11
16
 
12
17
  const MIN_IMAGE_DIMENSION = 300;
18
+ const MAX_IMAGE_DIMENSION = 1536;
13
19
 
14
20
  /**
15
21
  * Get image dimensions from URI
@@ -21,23 +27,39 @@ function getImageSize(uri: string): Promise<{ width: number; height: number }> {
21
27
  }
22
28
 
23
29
  /**
24
- * Ensure image meets minimum dimensions (300x300) required by AI providers.
25
- * Returns the original URI if already large enough, or a resized URI.
30
+ * Ensure image is within optimal dimensions for AI generation.
31
+ * - Too small (<300px): scale up (AI providers require minimum dimensions)
32
+ * - Too large (>1536px): scale down (reduces upload size, prevents timeouts)
33
+ * - Within range: return as-is
26
34
  */
27
- async function ensureMinimumSize(uri: string): Promise<string> {
35
+ async function ensureOptimalSize(uri: string): Promise<string> {
28
36
  try {
29
37
  const { width, height } = await getImageSize(uri);
38
+ const maxDim = Math.max(width, height);
30
39
 
31
- if (width >= MIN_IMAGE_DIMENSION && height >= MIN_IMAGE_DIMENSION) {
40
+ // Already within optimal range
41
+ if (width >= MIN_IMAGE_DIMENSION && height >= MIN_IMAGE_DIMENSION && maxDim <= MAX_IMAGE_DIMENSION) {
32
42
  return uri;
33
43
  }
34
44
 
35
- const scale = Math.max(MIN_IMAGE_DIMENSION / width, MIN_IMAGE_DIMENSION / height);
36
- const newWidth = Math.ceil(width * scale);
37
- const newHeight = Math.ceil(height * scale);
45
+ let newWidth: number;
46
+ let newHeight: number;
47
+
48
+ if (maxDim > MAX_IMAGE_DIMENSION) {
49
+ // Scale DOWN — largest dimension becomes MAX_IMAGE_DIMENSION
50
+ const scale = MAX_IMAGE_DIMENSION / maxDim;
51
+ newWidth = Math.round(width * scale);
52
+ newHeight = Math.round(height * scale);
53
+ } else {
54
+ // Scale UP — smallest dimension becomes MIN_IMAGE_DIMENSION
55
+ const scale = Math.max(MIN_IMAGE_DIMENSION / width, MIN_IMAGE_DIMENSION / height);
56
+ newWidth = Math.ceil(width * scale);
57
+ newHeight = Math.ceil(height * scale);
58
+ }
38
59
 
39
60
  if (typeof __DEV__ !== "undefined" && __DEV__) {
40
- console.log("[PhotoExtraction] Resizing small image", {
61
+ const direction = maxDim > MAX_IMAGE_DIMENSION ? "down" : "up";
62
+ console.log(`[PhotoExtraction] Resizing ${direction}`, {
41
63
  from: `${width}x${height}`,
42
64
  to: `${newWidth}x${newHeight}`,
43
65
  });
@@ -45,7 +67,7 @@ async function ensureMinimumSize(uri: string): Promise<string> {
45
67
 
46
68
  const result = await manipulateAsync(uri, [{ resize: { width: newWidth, height: newHeight } }], {
47
69
  format: SaveFormat.JPEG,
48
- compress: 0.9,
70
+ compress: maxDim > MAX_IMAGE_DIMENSION ? 0.8 : 0.9,
49
71
  });
50
72
 
51
73
  return result.uri;
@@ -105,8 +127,8 @@ export async function extractPhotosAsBase64(
105
127
  const results = await Promise.allSettled(
106
128
  photoUris.map(async (uri, index) => {
107
129
  try {
108
- const resizedUri = await ensureMinimumSize(uri);
109
- return await readFileAsBase64(resizedUri);
130
+ const optimizedUri = await ensureOptimalSize(uri);
131
+ return await readFileAsBase64(optimizedUri);
110
132
  } catch (error) {
111
133
  if (typeof __DEV__ !== "undefined" && __DEV__) {
112
134
  console.error(`[PhotoExtraction] Failed to read photo ${index}:`, error);
@@ -1,13 +1,44 @@
1
1
  /**
2
2
  * Creation Update Operations
3
- * Infrastructure: Updates creation status
3
+ * Infrastructure: Updates creation status and flushes generation logs to Firestore
4
4
  */
5
5
 
6
+ import { getFirestore, doc, setDoc } from "firebase/firestore";
6
7
  import type { ICreationsRepository } from "../../../../creations/domain/repositories";
7
8
  import type { Creation, CreationOutput } from "../../../../creations/domain/entities/Creation";
8
9
  import type { CompletedCreationData } from "./creation-persistence.types";
10
+ import { consumeGenerationLogs } from "../../../../../infrastructure/services/generation-log-store";
9
11
 
10
12
 
13
+ /**
14
+ * Flush generation logs to Firestore subcollection.
15
+ * Writes to: users/{userId}/creations/{creationId}/logs/session
16
+ * Non-blocking: errors are caught and logged, never propagated.
17
+ */
18
+ async function flushLogsToFirestore(
19
+ userId: string,
20
+ creationId: string,
21
+ ): Promise<void> {
22
+ const logs = consumeGenerationLogs();
23
+ if (logs.length === 0) return;
24
+
25
+ try {
26
+ const db = getFirestore();
27
+ const logDocRef = doc(db, 'users', userId, 'creations', creationId, 'logs', 'session');
28
+ await setDoc(logDocRef, {
29
+ entries: logs,
30
+ count: logs.length,
31
+ createdAt: new Date(),
32
+ });
33
+ } catch (error) {
34
+ // Never let log flushing break the main flow
35
+ console.warn(
36
+ '[CreationPersistence] Failed to flush logs to Firestore:',
37
+ error instanceof Error ? error.message : String(error),
38
+ );
39
+ }
40
+ }
41
+
11
42
  /**
12
43
  * Update creation to status="completed" when generation finishes
13
44
  */
@@ -37,9 +68,8 @@ export async function updateToCompleted(
37
68
  ...(durationMs !== undefined && { durationMs }),
38
69
  } as Partial<Creation>);
39
70
 
40
- if (typeof __DEV__ !== "undefined" && __DEV__) {
41
- console.log("[CreationPersistence] Updated to completed", { creationId });
42
- }
71
+ // Flush generation logs to Firestore awaited to ensure logs are persisted
72
+ await flushLogsToFirestore(userId, creationId);
43
73
  }
44
74
 
45
75
  /**
@@ -57,9 +87,8 @@ export async function updateToFailed(
57
87
  completedAt: new Date(),
58
88
  } as Partial<Creation>);
59
89
 
60
- if (typeof __DEV__ !== "undefined" && __DEV__) {
61
- console.log("[CreationPersistence] Updated to failed", { creationId, error });
62
- }
90
+ // Flush generation logs to Firestore awaited to ensure logs are persisted
91
+ await flushLogsToFirestore(userId, creationId);
63
92
  }
64
93
 
65
94
  /**
@@ -76,8 +105,4 @@ export async function updateRequestId(
76
105
  requestId,
77
106
  model,
78
107
  });
79
-
80
- if (typeof __DEV__ !== "undefined" && __DEV__) {
81
- console.log("[CreationPersistence] Updated requestId", { creationId, requestId, model });
82
- }
83
108
  }
@@ -111,6 +111,7 @@ export const GenericPhotoUploadScreen: React.FC<PhotoUploadScreenProps> = ({
111
111
  { key: "photoUpload.tips.goodLighting", icon: "sunny-outline" },
112
112
  { key: "photoUpload.tips.recentPhoto", icon: "time-outline" },
113
113
  { key: "photoUpload.tips.noFilters", icon: "image-outline" },
114
+ { key: "photoUpload.tips.useWifi", icon: "wifi-outline" },
114
115
  ];
115
116
  return tipKeys.map(({ key, icon }) => ({ text: t(key), icon }));
116
117
  }, [t]);
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Generation Log Store
3
+ * Temporarily holds log entries from the most recent generation session.
4
+ * The persistence layer reads from here when saving completed/failed creations.
5
+ *
6
+ * Flow:
7
+ * 1. Executor calls provider.subscribe() -> provider collects logs internally
8
+ * 2. Executor calls provider.endLogSession() -> gets log entries
9
+ * 3. Executor appends provider logs + its own entries here
10
+ * 4. Persistence layer reads logs from here when writing to Firestore
11
+ *
12
+ * IMPORTANT: Always use addGenerationLogs/addGenerationLog to append.
13
+ * clearGenerationLogs should be called at the start of a new generation.
14
+ */
15
+
16
+ import type { ProviderLogEntry } from "../../domain/interfaces/ai-provider.interface";
17
+
18
+ let sessionLogs: ProviderLogEntry[] = [];
19
+ let sessionStartTime: number = 0;
20
+
21
+ /**
22
+ * Clear logs and start a new session. Call at the beginning of each generation.
23
+ */
24
+ export function startGenerationLogSession(): void {
25
+ sessionLogs = [];
26
+ sessionStartTime = Date.now();
27
+ }
28
+
29
+ /**
30
+ * Append log entries to the current session (e.g. provider logs)
31
+ */
32
+ export function addGenerationLogs(logs: ProviderLogEntry[]): void {
33
+ sessionLogs.push(...logs);
34
+ }
35
+
36
+ /**
37
+ * Add a single log entry with proper elapsed calculation
38
+ */
39
+ export function addGenerationLog(
40
+ tag: string,
41
+ message: string,
42
+ level: ProviderLogEntry['level'] = 'info',
43
+ data?: Record<string, unknown>,
44
+ ): void {
45
+ const now = Date.now();
46
+ sessionLogs.push({
47
+ timestamp: now,
48
+ elapsed: sessionStartTime > 0 ? now - sessionStartTime : 0,
49
+ level,
50
+ tag,
51
+ message,
52
+ ...(data && { data }),
53
+ });
54
+
55
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
56
+ const fn = level === 'error' ? console.error : level === 'warn' ? console.warn : console.log;
57
+ fn(`[${tag}] ${message}`, data ?? '');
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Get and clear all stored logs (consume pattern)
63
+ */
64
+ export function consumeGenerationLogs(): ProviderLogEntry[] {
65
+ const logs = [...sessionLogs];
66
+ sessionLogs = [];
67
+ sessionStartTime = 0;
68
+ return logs;
69
+ }
70
+
71
+ /**
72
+ * Get logs without clearing
73
+ */
74
+ export function getGenerationLogs(): ProviderLogEntry[] {
75
+ return [...sessionLogs];
76
+ }
77
+
78
+ /**
79
+ * Clear stored logs
80
+ */
81
+ export function clearGenerationLogs(): void {
82
+ sessionLogs = [];
83
+ sessionStartTime = 0;
84
+ }
@@ -7,6 +7,7 @@ import { validateProvider } from "../utils/provider-validator.util";
7
7
  import { formatBase64 } from "../utils/base64.util";
8
8
  import { validateURL } from "../validation/base-validator";
9
9
  import { env } from "../config/env.config";
10
+ import { addGenerationLogs, addGenerationLog, startGenerationLogSession } from "./generation-log-store";
10
11
 
11
12
 
12
13
  /** Generation timeout in milliseconds */
@@ -45,23 +46,28 @@ export interface MultiImageGenerationResult {
45
46
  export async function executeMultiImageGeneration(
46
47
  input: MultiImageGenerationInput,
47
48
  ): Promise<MultiImageGenerationResult> {
49
+ const TAG = 'MultiImageExecutor';
50
+ const startTime = Date.now();
51
+ startGenerationLogSession();
52
+
48
53
  const validation = validateProvider("MultiImageExecutor");
49
54
  if (!validation.success) {
55
+ addGenerationLog(TAG, 'Provider validation failed', 'error');
50
56
  return { success: false, error: ("error" in validation ? validation.error : "Provider validation failed") };
51
57
  }
52
58
  const provider = validation.provider;
53
59
 
54
60
  try {
55
61
  const imageUrls = input.photos.map(formatBase64);
62
+ const totalImageSize = imageUrls.reduce((sum, url) => sum + url.length, 0);
56
63
 
57
- if (typeof __DEV__ !== "undefined" && __DEV__) {
58
- console.log("[MultiImageExecutor] Generation started", {
59
- imageCount: imageUrls.length,
60
- hasModel: !!input.model,
61
- hasPrompt: !!input.prompt,
62
- timestamp: new Date().toISOString(),
63
- });
64
- }
64
+ addGenerationLog(TAG, 'Generation starting', 'info', {
65
+ model: input.model,
66
+ imageCount: imageUrls.length,
67
+ totalImageSizeKB: Math.round(totalImageSize / 1024),
68
+ promptLength: input.prompt.length,
69
+ timeout: GENERATION_TIMEOUT_MS,
70
+ });
65
71
 
66
72
  const modelInput: Record<string, unknown> = {
67
73
  prompt: input.prompt,
@@ -72,14 +78,18 @@ export async function executeMultiImageGeneration(
72
78
  enable_safety_checker: MODEL_INPUT_DEFAULTS.enableSafetyChecker,
73
79
  };
74
80
 
81
+ addGenerationLog(TAG, 'Calling provider.subscribe()...');
75
82
  const result = await provider.subscribe(input.model, modelInput, {
76
83
  timeoutMs: GENERATION_TIMEOUT_MS,
77
84
  });
78
85
 
86
+ // Collect provider logs
87
+ const providerLogs = provider.endLogSession?.() ?? provider.getSessionLogs?.() ?? [];
88
+ addGenerationLogs(providerLogs);
89
+
79
90
  const rawResult = result as Record<string, unknown>;
80
91
  const data = rawResult?.data ?? rawResult;
81
92
 
82
- // Type-safe extraction of image URL
83
93
  if (data && typeof data === "object" && "images" in data) {
84
94
  const images = (data as { images?: unknown }).images;
85
95
  if (Array.isArray(images) && images.length > 0) {
@@ -87,26 +97,36 @@ export async function executeMultiImageGeneration(
87
97
  if (firstImage && typeof firstImage === "object" && "url" in firstImage) {
88
98
  const imageUrl = (firstImage as { url?: unknown }).url;
89
99
  if (typeof imageUrl === "string" && imageUrl.length > 0) {
90
- // Validate URL for security
91
100
  const urlValidation = validateURL(imageUrl);
92
101
  if (!urlValidation.isValid) {
102
+ addGenerationLog(TAG, `Invalid URL in response: ${urlValidation.errors.join(", ")}`, 'error');
93
103
  return {
94
104
  success: false,
95
105
  error: `Invalid image URL received: ${urlValidation.errors.join(", ")}`
96
106
  };
97
107
  }
108
+ const elapsed = Date.now() - startTime;
109
+ addGenerationLog(TAG, `Generation SUCCESS in ${elapsed}ms`, 'info', { imageUrl, elapsed });
98
110
  return { success: true, imageUrl };
99
111
  }
100
112
  }
101
113
  }
102
114
  }
103
115
 
116
+ const elapsed = Date.now() - startTime;
117
+ addGenerationLog(TAG, `No image in response after ${elapsed}ms`, 'error', {
118
+ dataKeys: data && typeof data === "object" ? Object.keys(data) : [],
119
+ elapsed,
120
+ });
104
121
  return { success: false, error: "No image generated" };
105
122
  } catch (error) {
123
+ // Collect provider logs even on failure
124
+ const providerLogs = provider.endLogSession?.() ?? provider.getSessionLogs?.() ?? [];
125
+ addGenerationLogs(providerLogs);
126
+
127
+ const elapsed = Date.now() - startTime;
106
128
  const message = error instanceof Error ? error.message : "Generation failed";
107
- if (typeof __DEV__ !== "undefined" && __DEV__) {
108
- console.error("[MultiImageExecutor] Error:", message);
109
- }
129
+ addGenerationLog(TAG, `Generation FAILED after ${elapsed}ms: ${message}`, 'error', { elapsed });
110
130
  return { success: false, error: message };
111
131
  }
112
132
  }