@umituz/react-native-ai-generation-content 1.83.76 → 1.83.78
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 +1 -1
- package/src/domains/generation/infrastructure/executors/image-executor.ts +10 -11
- package/src/domains/generation/wizard/infrastructure/strategies/image-generation.executor.ts +15 -14
- package/src/domains/generation/wizard/infrastructure/strategies/image-generation.strategy.ts +4 -2
- package/src/domains/generation/wizard/infrastructure/strategies/shared/photo-extraction.utils.ts +33 -11
- package/src/domains/generation/wizard/infrastructure/utils/creation-persistence-factory.ts +2 -2
- package/src/domains/generation/wizard/infrastructure/utils/creation-persistence.types.ts +2 -0
- package/src/domains/generation/wizard/infrastructure/utils/creation-update-operations.ts +7 -4
- package/src/domains/generation/wizard/presentation/hooks/usePhotoBlockingGeneration.ts +6 -3
- package/src/domains/generation/wizard/presentation/screens/GenericPhotoUploadScreen.tsx +1 -0
- package/src/infrastructure/services/generation-log-store.ts +48 -32
- package/src/infrastructure/services/multi-image-generation.executor.ts +17 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.83.
|
|
3
|
+
"version": "1.83.78",
|
|
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",
|
|
@@ -25,12 +25,11 @@ export class ImageExecutor
|
|
|
25
25
|
): Promise<GenerationResult<ImageGenerationOutput>> {
|
|
26
26
|
const TAG = 'GenericImageExecutor';
|
|
27
27
|
const startTime = Date.now();
|
|
28
|
-
|
|
29
|
-
startGenerationLogSession();
|
|
28
|
+
const sid = startGenerationLogSession();
|
|
30
29
|
|
|
31
30
|
try {
|
|
32
31
|
const totalImageSize = input.imageUrls?.reduce((sum, url) => sum + url.length, 0) ?? 0;
|
|
33
|
-
addGenerationLog(TAG, 'Starting generation', 'info', {
|
|
32
|
+
addGenerationLog(sid, TAG, 'Starting generation', 'info', {
|
|
34
33
|
model,
|
|
35
34
|
imageCount: input.imageUrls?.length || 0,
|
|
36
35
|
totalImageSizeKB: Math.round(totalImageSize / 1024),
|
|
@@ -40,19 +39,19 @@ export class ImageExecutor
|
|
|
40
39
|
const provider = providerRegistry.getActiveProvider();
|
|
41
40
|
|
|
42
41
|
if (!provider?.isInitialized()) {
|
|
43
|
-
addGenerationLog(TAG, 'Provider not initialized!', 'error');
|
|
42
|
+
addGenerationLog(sid, TAG, 'Provider not initialized!', 'error');
|
|
44
43
|
return { success: false, error: "AI provider not initialized" };
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
options?.onProgress?.(5);
|
|
48
47
|
const modelInput = this.buildModelInput(input);
|
|
49
|
-
addGenerationLog(TAG, 'Model input prepared', 'info', {
|
|
48
|
+
addGenerationLog(sid, TAG, 'Model input prepared', 'info', {
|
|
50
49
|
imageCount: modelInput.image_urls?.length || 0,
|
|
51
50
|
aspectRatio: modelInput.aspect_ratio,
|
|
52
51
|
});
|
|
53
52
|
|
|
54
53
|
options?.onProgress?.(10);
|
|
55
|
-
addGenerationLog(TAG, 'Calling provider.subscribe()...');
|
|
54
|
+
addGenerationLog(sid, TAG, 'Calling provider.subscribe()...');
|
|
56
55
|
|
|
57
56
|
const result = await provider.subscribe(model, modelInput, {
|
|
58
57
|
timeoutMs: options?.timeoutMs ?? env.generationImageTimeoutMs,
|
|
@@ -67,31 +66,31 @@ export class ImageExecutor
|
|
|
67
66
|
|
|
68
67
|
// Collect provider logs
|
|
69
68
|
const providerLogs = provider.endLogSession?.() ?? provider.getSessionLogs?.() ?? [];
|
|
70
|
-
addGenerationLogs(providerLogs);
|
|
69
|
+
addGenerationLogs(sid, providerLogs);
|
|
71
70
|
|
|
72
71
|
options?.onProgress?.(90);
|
|
73
72
|
const imageUrl = this.extractImageUrl(result);
|
|
74
73
|
const elapsed = Date.now() - startTime;
|
|
75
74
|
|
|
76
75
|
if (!imageUrl) {
|
|
77
|
-
addGenerationLog(TAG, `No image URL in response after ${elapsed}ms`, 'error');
|
|
76
|
+
addGenerationLog(sid, TAG, `No image URL in response after ${elapsed}ms`, 'error');
|
|
78
77
|
return { success: false, error: "No image generated" };
|
|
79
78
|
}
|
|
80
79
|
|
|
81
80
|
options?.onProgress?.(100);
|
|
82
|
-
addGenerationLog(TAG, `Generation SUCCESS in ${elapsed}ms`, 'info', { imageUrl, elapsed });
|
|
81
|
+
addGenerationLog(sid, TAG, `Generation SUCCESS in ${elapsed}ms`, 'info', { imageUrl, elapsed });
|
|
83
82
|
return { success: true, data: { imageUrl } };
|
|
84
83
|
} catch (error) {
|
|
85
84
|
// Collect provider logs even on failure
|
|
86
85
|
const provider = providerRegistry.getActiveProvider();
|
|
87
86
|
if (provider) {
|
|
88
87
|
const providerLogs = provider.endLogSession?.() ?? provider.getSessionLogs?.() ?? [];
|
|
89
|
-
addGenerationLogs(providerLogs);
|
|
88
|
+
addGenerationLogs(sid, providerLogs);
|
|
90
89
|
}
|
|
91
90
|
|
|
92
91
|
const elapsed = Date.now() - startTime;
|
|
93
92
|
const message = error instanceof Error ? error.message : "Generation failed";
|
|
94
|
-
addGenerationLog(TAG, `Generation FAILED after ${elapsed}ms: ${message}`, 'error', { elapsed });
|
|
93
|
+
addGenerationLog(sid, TAG, `Generation FAILED after ${elapsed}ms: ${message}`, 'error', { elapsed });
|
|
95
94
|
return { success: false, error: message };
|
|
96
95
|
}
|
|
97
96
|
}
|
package/src/domains/generation/wizard/infrastructure/strategies/image-generation.executor.ts
CHANGED
|
@@ -18,6 +18,7 @@ interface ExecutionResult {
|
|
|
18
18
|
success: boolean;
|
|
19
19
|
imageUrl?: string;
|
|
20
20
|
error?: string;
|
|
21
|
+
logSessionId?: string;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
function formatBase64(base64: string): string {
|
|
@@ -54,13 +55,13 @@ export async function executeImageGeneration(
|
|
|
54
55
|
): Promise<ExecutionResult> {
|
|
55
56
|
const TAG = 'ImageExecutor';
|
|
56
57
|
const startTime = Date.now();
|
|
57
|
-
startGenerationLogSession();
|
|
58
|
+
const sid = startGenerationLogSession();
|
|
58
59
|
const { providerRegistry } = await import("../../../../../infrastructure/services/provider-registry.service");
|
|
59
60
|
|
|
60
61
|
const provider = providerRegistry.getActiveProvider();
|
|
61
62
|
if (!provider?.isInitialized()) {
|
|
62
|
-
addGenerationLog(TAG, 'Provider not initialized!', 'error');
|
|
63
|
-
return { success: false, error: "AI provider not initialized" };
|
|
63
|
+
addGenerationLog(sid, TAG, 'Provider not initialized!', 'error');
|
|
64
|
+
return { success: false, error: "AI provider not initialized", logSessionId: sid };
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
try {
|
|
@@ -69,14 +70,14 @@ export async function executeImageGeneration(
|
|
|
69
70
|
const mode = imageUrls.length > 0 ? "Photo-based" : "Text-to-image";
|
|
70
71
|
const totalImageSize = imageUrls.reduce((sum, url) => sum + url.length, 0);
|
|
71
72
|
|
|
72
|
-
addGenerationLog(TAG, `${mode} generation starting`, 'info', {
|
|
73
|
+
addGenerationLog(sid, TAG, `${mode} generation starting`, 'info', {
|
|
73
74
|
model,
|
|
74
75
|
photoCount: imageUrls.length,
|
|
75
76
|
totalImageSizeKB: Math.round(totalImageSize / 1024),
|
|
76
77
|
promptLength: finalPrompt.length,
|
|
77
78
|
timeout: GENERATION_TIMEOUT_MS,
|
|
78
79
|
});
|
|
79
|
-
addGenerationLog(TAG, `Prompt: ${finalPrompt.substring(0, 300)}${finalPrompt.length > 300 ? "..." : ""}`);
|
|
80
|
+
addGenerationLog(sid, TAG, `Prompt: ${finalPrompt.substring(0, 300)}${finalPrompt.length > 300 ? "..." : ""}`);
|
|
80
81
|
|
|
81
82
|
const modelInput: Record<string, unknown> = {
|
|
82
83
|
prompt: finalPrompt,
|
|
@@ -92,7 +93,7 @@ export async function executeImageGeneration(
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
let lastStatus = "";
|
|
95
|
-
addGenerationLog(TAG, 'Calling provider.subscribe()...');
|
|
96
|
+
addGenerationLog(sid, TAG, 'Calling provider.subscribe()...');
|
|
96
97
|
const result = await provider.subscribe(model, modelInput, {
|
|
97
98
|
timeoutMs: GENERATION_TIMEOUT_MS,
|
|
98
99
|
onQueueUpdate: (status) => {
|
|
@@ -104,7 +105,7 @@ export async function executeImageGeneration(
|
|
|
104
105
|
|
|
105
106
|
// Collect provider logs after subscribe completes
|
|
106
107
|
const providerLogs = provider.endLogSession?.() ?? provider.getSessionLogs?.() ?? [];
|
|
107
|
-
addGenerationLogs(providerLogs);
|
|
108
|
+
addGenerationLogs(sid, providerLogs);
|
|
108
109
|
|
|
109
110
|
const rawResult = result as Record<string, unknown>;
|
|
110
111
|
const data = (rawResult?.data ?? rawResult) as { images?: Array<{ url: string }> };
|
|
@@ -114,23 +115,23 @@ export async function executeImageGeneration(
|
|
|
114
115
|
onProgress?.(100);
|
|
115
116
|
|
|
116
117
|
if (imageUrl) {
|
|
117
|
-
addGenerationLog(TAG, `Generation SUCCESS in ${elapsed}ms`, 'info', { imageUrl, elapsed });
|
|
118
|
-
return { success: true, imageUrl };
|
|
118
|
+
addGenerationLog(sid, TAG, `Generation SUCCESS in ${elapsed}ms`, 'info', { imageUrl, elapsed });
|
|
119
|
+
return { success: true, imageUrl, logSessionId: sid };
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
addGenerationLog(TAG, `No image in response after ${elapsed}ms`, 'error', {
|
|
122
|
+
addGenerationLog(sid, TAG, `No image in response after ${elapsed}ms`, 'error', {
|
|
122
123
|
responseKeys: Object.keys(data || {}),
|
|
123
124
|
elapsed,
|
|
124
125
|
});
|
|
125
|
-
return { success: false, error: "No image generated" };
|
|
126
|
+
return { success: false, error: "No image generated", logSessionId: sid };
|
|
126
127
|
} catch (error) {
|
|
127
128
|
// Collect provider logs even on failure
|
|
128
129
|
const providerLogs = provider.endLogSession?.() ?? provider.getSessionLogs?.() ?? [];
|
|
129
|
-
addGenerationLogs(providerLogs);
|
|
130
|
+
addGenerationLogs(sid, providerLogs);
|
|
130
131
|
|
|
131
132
|
const elapsed = Date.now() - startTime;
|
|
132
133
|
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 };
|
|
134
|
+
addGenerationLog(sid, TAG, `Generation FAILED after ${elapsed}ms: ${errorMsg}`, 'error', { elapsed });
|
|
135
|
+
return { success: false, error: errorMsg, logSessionId: sid };
|
|
135
136
|
}
|
|
136
137
|
}
|
package/src/domains/generation/wizard/infrastructure/strategies/image-generation.strategy.ts
CHANGED
|
@@ -94,10 +94,12 @@ export function createImageStrategy(options: CreateImageStrategyOptions): Wizard
|
|
|
94
94
|
const result = await executeImageGeneration(imageInput, model);
|
|
95
95
|
|
|
96
96
|
if (!result.success || !result.imageUrl) {
|
|
97
|
-
|
|
97
|
+
const error = new Error(result.error || "Image generation failed");
|
|
98
|
+
(error as Error & { logSessionId?: string }).logSessionId = result.logSessionId;
|
|
99
|
+
throw error;
|
|
98
100
|
}
|
|
99
101
|
|
|
100
|
-
return { imageUrl: result.imageUrl };
|
|
102
|
+
return { imageUrl: result.imageUrl, logSessionId: result.logSessionId };
|
|
101
103
|
},
|
|
102
104
|
};
|
|
103
105
|
}
|
package/src/domains/generation/wizard/infrastructure/strategies/shared/photo-extraction.utils.ts
CHANGED
|
@@ -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
|
|
25
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
|
109
|
-
return await readFileAsBase64(
|
|
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);
|
|
@@ -20,8 +20,8 @@ export function createCreationPersistence() {
|
|
|
20
20
|
updateToCompleted: (userId: string, creationId: string, data: CompletedCreationData) =>
|
|
21
21
|
updateToCompleted(repository, userId, creationId, data),
|
|
22
22
|
|
|
23
|
-
updateToFailed: (userId: string, creationId: string, error: string) =>
|
|
24
|
-
updateToFailed(repository, userId, creationId, error),
|
|
23
|
+
updateToFailed: (userId: string, creationId: string, error: string, logSessionId?: string) =>
|
|
24
|
+
updateToFailed(repository, userId, creationId, error, logSessionId),
|
|
25
25
|
|
|
26
26
|
updateRequestId: (userId: string, creationId: string, requestId: string, model: string) =>
|
|
27
27
|
updateRequestId(repository, userId, creationId, requestId, model),
|
|
@@ -24,4 +24,6 @@ export interface CompletedCreationData {
|
|
|
24
24
|
readonly thumbnailUrl?: string;
|
|
25
25
|
/** Unix timestamp (ms) when generation was submitted; used to compute durationMs */
|
|
26
26
|
readonly generationStartedAt?: number;
|
|
27
|
+
/** Log session ID for flushing session-scoped generation logs */
|
|
28
|
+
readonly logSessionId?: string;
|
|
27
29
|
}
|
|
@@ -18,8 +18,10 @@ import { consumeGenerationLogs } from "../../../../../infrastructure/services/ge
|
|
|
18
18
|
async function flushLogsToFirestore(
|
|
19
19
|
userId: string,
|
|
20
20
|
creationId: string,
|
|
21
|
+
logSessionId?: string,
|
|
21
22
|
): Promise<void> {
|
|
22
|
-
|
|
23
|
+
if (!logSessionId) return;
|
|
24
|
+
const logs = consumeGenerationLogs(logSessionId);
|
|
23
25
|
if (logs.length === 0) return;
|
|
24
26
|
|
|
25
27
|
try {
|
|
@@ -69,7 +71,7 @@ export async function updateToCompleted(
|
|
|
69
71
|
} as Partial<Creation>);
|
|
70
72
|
|
|
71
73
|
// Flush generation logs to Firestore — awaited to ensure logs are persisted
|
|
72
|
-
await flushLogsToFirestore(userId, creationId);
|
|
74
|
+
await flushLogsToFirestore(userId, creationId, data.logSessionId);
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
/**
|
|
@@ -79,7 +81,8 @@ export async function updateToFailed(
|
|
|
79
81
|
repository: ICreationsRepository,
|
|
80
82
|
userId: string,
|
|
81
83
|
creationId: string,
|
|
82
|
-
error: string
|
|
84
|
+
error: string,
|
|
85
|
+
logSessionId?: string,
|
|
83
86
|
): Promise<void> {
|
|
84
87
|
await repository.update(userId, creationId, {
|
|
85
88
|
status: "failed" as const,
|
|
@@ -88,7 +91,7 @@ export async function updateToFailed(
|
|
|
88
91
|
} as Partial<Creation>);
|
|
89
92
|
|
|
90
93
|
// Flush generation logs to Firestore — awaited to ensure logs are persisted
|
|
91
|
-
await flushLogsToFirestore(userId, creationId);
|
|
94
|
+
await flushLogsToFirestore(userId, creationId, logSessionId);
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
/**
|
|
@@ -48,10 +48,11 @@ export function usePhotoBlockingGeneration(
|
|
|
48
48
|
} = props;
|
|
49
49
|
|
|
50
50
|
const creationIdRef = useRef<string | null>(null);
|
|
51
|
+
const logSessionIdRef = useRef<string | undefined>(undefined);
|
|
51
52
|
|
|
52
53
|
const handleSuccess = useCallback(
|
|
53
54
|
async (result: unknown) => {
|
|
54
|
-
const typedResult = result as { imageUrl?: string; videoUrl?: string };
|
|
55
|
+
const typedResult = result as { imageUrl?: string; videoUrl?: string; logSessionId?: string };
|
|
55
56
|
const creationId = creationIdRef.current;
|
|
56
57
|
|
|
57
58
|
if (creationId && userId) {
|
|
@@ -60,6 +61,7 @@ export function usePhotoBlockingGeneration(
|
|
|
60
61
|
uri: typedResult.imageUrl || typedResult.videoUrl || "",
|
|
61
62
|
imageUrl: typedResult.imageUrl,
|
|
62
63
|
videoUrl: typedResult.videoUrl,
|
|
64
|
+
logSessionId: typedResult.logSessionId,
|
|
63
65
|
});
|
|
64
66
|
} catch (err) {
|
|
65
67
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
@@ -85,12 +87,13 @@ export function usePhotoBlockingGeneration(
|
|
|
85
87
|
);
|
|
86
88
|
|
|
87
89
|
const handleError = useCallback(
|
|
88
|
-
async (err: { message: string }) => {
|
|
90
|
+
async (err: { message: string; originalError?: Error & { logSessionId?: string } }) => {
|
|
89
91
|
const creationId = creationIdRef.current;
|
|
92
|
+
const logSessionId = err.originalError?.logSessionId;
|
|
90
93
|
|
|
91
94
|
if (creationId && userId) {
|
|
92
95
|
try {
|
|
93
|
-
await persistence.updateToFailed(userId, creationId, err.message);
|
|
96
|
+
await persistence.updateToFailed(userId, creationId, err.message, logSessionId);
|
|
94
97
|
} catch (updateErr) {
|
|
95
98
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
96
99
|
console.error("[PhotoBlockingGeneration] updateToFailed error:", updateErr);
|
|
@@ -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]);
|
|
@@ -1,56 +1,71 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generation Log Store
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Session-scoped log buffer for generation executors.
|
|
4
|
+
* Each generation gets an isolated session — concurrent generations don't corrupt each other.
|
|
5
5
|
*
|
|
6
6
|
* Flow:
|
|
7
|
-
* 1. Executor calls
|
|
8
|
-
* 2. Executor calls
|
|
9
|
-
* 3. Executor
|
|
10
|
-
* 4.
|
|
11
|
-
*
|
|
12
|
-
* IMPORTANT: Always use addGenerationLogs/addGenerationLog to append.
|
|
13
|
-
* clearGenerationLogs should be called at the start of a new generation.
|
|
7
|
+
* 1. Executor calls startGenerationLogSession() → gets sessionId
|
|
8
|
+
* 2. Executor calls addGenerationLog(sessionId, ...) → logs accumulate
|
|
9
|
+
* 3. Executor calls provider.endLogSession() → gets provider logs
|
|
10
|
+
* 4. Executor calls addGenerationLogs(sessionId, providerLogs) → appends
|
|
11
|
+
* 5. Persistence layer calls consumeGenerationLogs(sessionId) → writes to Firestore
|
|
14
12
|
*/
|
|
15
13
|
|
|
16
14
|
import type { ProviderLogEntry } from "../../domain/interfaces/ai-provider.interface";
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
interface LogSession {
|
|
17
|
+
readonly startTime: number;
|
|
18
|
+
entries: ProviderLogEntry[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let sessionCounter = 0;
|
|
22
|
+
const sessions = new Map<string, LogSession>();
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
|
-
*
|
|
25
|
+
* Start a new isolated log session. Returns session ID.
|
|
26
|
+
* Call at the beginning of each generation.
|
|
23
27
|
*/
|
|
24
|
-
export function startGenerationLogSession():
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
export function startGenerationLogSession(): string {
|
|
29
|
+
const id = `gen_${++sessionCounter}_${Date.now()}`;
|
|
30
|
+
sessions.set(id, { startTime: Date.now(), entries: [] });
|
|
31
|
+
return id;
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
/**
|
|
30
|
-
* Append log entries to
|
|
35
|
+
* Append log entries to a specific session (e.g. provider logs)
|
|
31
36
|
*/
|
|
32
|
-
export function addGenerationLogs(logs: ProviderLogEntry[]): void {
|
|
33
|
-
|
|
37
|
+
export function addGenerationLogs(sessionId: string, logs: ProviderLogEntry[]): void {
|
|
38
|
+
const session = sessions.get(sessionId);
|
|
39
|
+
if (session) {
|
|
40
|
+
session.entries.push(...logs);
|
|
41
|
+
}
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
/**
|
|
37
45
|
* Add a single log entry with proper elapsed calculation
|
|
38
46
|
*/
|
|
39
47
|
export function addGenerationLog(
|
|
48
|
+
sessionId: string,
|
|
40
49
|
tag: string,
|
|
41
50
|
message: string,
|
|
42
51
|
level: ProviderLogEntry['level'] = 'info',
|
|
43
52
|
data?: Record<string, unknown>,
|
|
44
53
|
): void {
|
|
54
|
+
const session = sessions.get(sessionId);
|
|
45
55
|
const now = Date.now();
|
|
46
|
-
|
|
56
|
+
|
|
57
|
+
const entry: ProviderLogEntry = {
|
|
47
58
|
timestamp: now,
|
|
48
|
-
elapsed:
|
|
59
|
+
elapsed: session ? now - session.startTime : 0,
|
|
49
60
|
level,
|
|
50
61
|
tag,
|
|
51
62
|
message,
|
|
52
63
|
...(data && { data }),
|
|
53
|
-
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (session) {
|
|
67
|
+
session.entries.push(entry);
|
|
68
|
+
}
|
|
54
69
|
|
|
55
70
|
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
56
71
|
const fn = level === 'error' ? console.error : level === 'warn' ? console.warn : console.log;
|
|
@@ -59,26 +74,27 @@ export function addGenerationLog(
|
|
|
59
74
|
}
|
|
60
75
|
|
|
61
76
|
/**
|
|
62
|
-
* Get and clear all stored logs (consume pattern)
|
|
77
|
+
* Get and clear all stored logs for a session (consume pattern).
|
|
78
|
+
* Removes the session after consuming.
|
|
63
79
|
*/
|
|
64
|
-
export function consumeGenerationLogs(): ProviderLogEntry[] {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
80
|
+
export function consumeGenerationLogs(sessionId: string): ProviderLogEntry[] {
|
|
81
|
+
const session = sessions.get(sessionId);
|
|
82
|
+
if (!session) return [];
|
|
83
|
+
const logs = [...session.entries];
|
|
84
|
+
sessions.delete(sessionId);
|
|
68
85
|
return logs;
|
|
69
86
|
}
|
|
70
87
|
|
|
71
88
|
/**
|
|
72
89
|
* Get logs without clearing
|
|
73
90
|
*/
|
|
74
|
-
export function getGenerationLogs(): ProviderLogEntry[] {
|
|
75
|
-
return [...
|
|
91
|
+
export function getGenerationLogs(sessionId: string): ProviderLogEntry[] {
|
|
92
|
+
return [...(sessions.get(sessionId)?.entries ?? [])];
|
|
76
93
|
}
|
|
77
94
|
|
|
78
95
|
/**
|
|
79
|
-
* Clear
|
|
96
|
+
* Clear a specific session's logs
|
|
80
97
|
*/
|
|
81
|
-
export function clearGenerationLogs(): void {
|
|
82
|
-
|
|
83
|
-
sessionStartTime = 0;
|
|
98
|
+
export function clearGenerationLogs(sessionId: string): void {
|
|
99
|
+
sessions.delete(sessionId);
|
|
84
100
|
}
|
|
@@ -38,6 +38,7 @@ export interface MultiImageGenerationResult {
|
|
|
38
38
|
readonly success: boolean;
|
|
39
39
|
readonly imageUrl?: string;
|
|
40
40
|
readonly error?: string;
|
|
41
|
+
readonly logSessionId?: string;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
/**
|
|
@@ -48,12 +49,12 @@ export async function executeMultiImageGeneration(
|
|
|
48
49
|
): Promise<MultiImageGenerationResult> {
|
|
49
50
|
const TAG = 'MultiImageExecutor';
|
|
50
51
|
const startTime = Date.now();
|
|
51
|
-
startGenerationLogSession();
|
|
52
|
+
const sid = startGenerationLogSession();
|
|
52
53
|
|
|
53
54
|
const validation = validateProvider("MultiImageExecutor");
|
|
54
55
|
if (!validation.success) {
|
|
55
|
-
addGenerationLog(TAG, 'Provider validation failed', 'error');
|
|
56
|
-
return { success: false, error: ("error" in validation ? validation.error : "Provider validation failed") };
|
|
56
|
+
addGenerationLog(sid, TAG, 'Provider validation failed', 'error');
|
|
57
|
+
return { success: false, error: ("error" in validation ? validation.error : "Provider validation failed"), logSessionId: sid };
|
|
57
58
|
}
|
|
58
59
|
const provider = validation.provider;
|
|
59
60
|
|
|
@@ -61,7 +62,7 @@ export async function executeMultiImageGeneration(
|
|
|
61
62
|
const imageUrls = input.photos.map(formatBase64);
|
|
62
63
|
const totalImageSize = imageUrls.reduce((sum, url) => sum + url.length, 0);
|
|
63
64
|
|
|
64
|
-
addGenerationLog(TAG, 'Generation starting', 'info', {
|
|
65
|
+
addGenerationLog(sid, TAG, 'Generation starting', 'info', {
|
|
65
66
|
model: input.model,
|
|
66
67
|
imageCount: imageUrls.length,
|
|
67
68
|
totalImageSizeKB: Math.round(totalImageSize / 1024),
|
|
@@ -78,14 +79,14 @@ export async function executeMultiImageGeneration(
|
|
|
78
79
|
enable_safety_checker: MODEL_INPUT_DEFAULTS.enableSafetyChecker,
|
|
79
80
|
};
|
|
80
81
|
|
|
81
|
-
addGenerationLog(TAG, 'Calling provider.subscribe()...');
|
|
82
|
+
addGenerationLog(sid, TAG, 'Calling provider.subscribe()...');
|
|
82
83
|
const result = await provider.subscribe(input.model, modelInput, {
|
|
83
84
|
timeoutMs: GENERATION_TIMEOUT_MS,
|
|
84
85
|
});
|
|
85
86
|
|
|
86
87
|
// Collect provider logs
|
|
87
88
|
const providerLogs = provider.endLogSession?.() ?? provider.getSessionLogs?.() ?? [];
|
|
88
|
-
addGenerationLogs(providerLogs);
|
|
89
|
+
addGenerationLogs(sid, providerLogs);
|
|
89
90
|
|
|
90
91
|
const rawResult = result as Record<string, unknown>;
|
|
91
92
|
const data = rawResult?.data ?? rawResult;
|
|
@@ -99,34 +100,35 @@ export async function executeMultiImageGeneration(
|
|
|
99
100
|
if (typeof imageUrl === "string" && imageUrl.length > 0) {
|
|
100
101
|
const urlValidation = validateURL(imageUrl);
|
|
101
102
|
if (!urlValidation.isValid) {
|
|
102
|
-
addGenerationLog(TAG, `Invalid URL in response: ${urlValidation.errors.join(", ")}`, 'error');
|
|
103
|
+
addGenerationLog(sid, TAG, `Invalid URL in response: ${urlValidation.errors.join(", ")}`, 'error');
|
|
103
104
|
return {
|
|
104
105
|
success: false,
|
|
105
|
-
error: `Invalid image URL received: ${urlValidation.errors.join(", ")}
|
|
106
|
+
error: `Invalid image URL received: ${urlValidation.errors.join(", ")}`,
|
|
107
|
+
logSessionId: sid,
|
|
106
108
|
};
|
|
107
109
|
}
|
|
108
110
|
const elapsed = Date.now() - startTime;
|
|
109
|
-
addGenerationLog(TAG, `Generation SUCCESS in ${elapsed}ms`, 'info', { imageUrl, elapsed });
|
|
110
|
-
return { success: true, imageUrl };
|
|
111
|
+
addGenerationLog(sid, TAG, `Generation SUCCESS in ${elapsed}ms`, 'info', { imageUrl, elapsed });
|
|
112
|
+
return { success: true, imageUrl, logSessionId: sid };
|
|
111
113
|
}
|
|
112
114
|
}
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
const elapsed = Date.now() - startTime;
|
|
117
|
-
addGenerationLog(TAG, `No image in response after ${elapsed}ms`, 'error', {
|
|
119
|
+
addGenerationLog(sid, TAG, `No image in response after ${elapsed}ms`, 'error', {
|
|
118
120
|
dataKeys: data && typeof data === "object" ? Object.keys(data) : [],
|
|
119
121
|
elapsed,
|
|
120
122
|
});
|
|
121
|
-
return { success: false, error: "No image generated" };
|
|
123
|
+
return { success: false, error: "No image generated", logSessionId: sid };
|
|
122
124
|
} catch (error) {
|
|
123
125
|
// Collect provider logs even on failure
|
|
124
126
|
const providerLogs = provider.endLogSession?.() ?? provider.getSessionLogs?.() ?? [];
|
|
125
|
-
addGenerationLogs(providerLogs);
|
|
127
|
+
addGenerationLogs(sid, providerLogs);
|
|
126
128
|
|
|
127
129
|
const elapsed = Date.now() - startTime;
|
|
128
130
|
const message = error instanceof Error ? error.message : "Generation failed";
|
|
129
|
-
addGenerationLog(TAG, `Generation FAILED after ${elapsed}ms: ${message}`, 'error', { elapsed });
|
|
130
|
-
return { success: false, error: message };
|
|
131
|
+
addGenerationLog(sid, TAG, `Generation FAILED after ${elapsed}ms: ${message}`, 'error', { elapsed });
|
|
132
|
+
return { success: false, error: message, logSessionId: sid };
|
|
131
133
|
}
|
|
132
134
|
}
|