@umituz/react-native-ai-generation-content 1.48.2 → 1.50.0
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/creations/domain/entities/Creation.ts +8 -0
- package/src/domains/creations/index.ts +5 -0
- package/src/domains/creations/infrastructure/repositories/CreationsWriter.ts +3 -0
- package/src/domains/creations/presentation/hooks/useProcessingJobsPoller.ts +163 -0
- package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +9 -0
- package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts +30 -1
- package/src/domains/generation/wizard/infrastructure/strategies/wizard-strategy.constants.ts +3 -0
- package/src/domains/generation/wizard/infrastructure/strategies/wizard-strategy.types.ts +10 -1
- package/src/domains/generation/wizard/infrastructure/utils/creation-persistence.util.ts +30 -1
- package/src/domains/generation/wizard/presentation/hooks/generation-result.utils.ts +47 -0
- package/src/domains/generation/wizard/presentation/hooks/usePhotoBlockingGeneration.ts +132 -0
- package/src/domains/generation/wizard/presentation/hooks/useVideoQueueGeneration.ts +169 -0
- package/src/domains/generation/wizard/presentation/hooks/useWizardGeneration.ts +50 -85
- package/src/index.ts +1 -0
- package/src/infrastructure/services/index.ts +1 -0
- package/src/infrastructure/services/video-feature-executor.service.ts +61 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.50.0",
|
|
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",
|
|
@@ -35,6 +35,9 @@ export interface Creation {
|
|
|
35
35
|
// Extended fields for job-based creations
|
|
36
36
|
readonly status?: CreationStatus;
|
|
37
37
|
readonly output?: CreationOutput;
|
|
38
|
+
// Background job tracking - FAL queue requestId and model
|
|
39
|
+
readonly requestId?: string;
|
|
40
|
+
readonly model?: string;
|
|
38
41
|
// Soft delete - if set, the creation is considered deleted
|
|
39
42
|
readonly deletedAt?: Date;
|
|
40
43
|
}
|
|
@@ -58,6 +61,9 @@ export interface CreationDocument {
|
|
|
58
61
|
readonly createdAt: FirebaseTimestamp | Date;
|
|
59
62
|
readonly completedAt?: FirebaseTimestamp | Date;
|
|
60
63
|
readonly deletedAt?: FirebaseTimestamp | Date;
|
|
64
|
+
// Background job tracking - FAL queue requestId and model
|
|
65
|
+
readonly requestId?: string;
|
|
66
|
+
readonly model?: string;
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
interface FirebaseTimestamp {
|
|
@@ -113,6 +119,8 @@ export function mapDocumentToCreation(
|
|
|
113
119
|
ratedAt: ratedAtDate,
|
|
114
120
|
status: data.status as CreationStatus | undefined,
|
|
115
121
|
output: data.output ?? undefined,
|
|
122
|
+
requestId: data.requestId,
|
|
123
|
+
model: data.model,
|
|
116
124
|
deletedAt: deletedAtDate,
|
|
117
125
|
};
|
|
118
126
|
}
|
|
@@ -125,6 +125,11 @@ export type {
|
|
|
125
125
|
BaseProcessingStartData,
|
|
126
126
|
BaseProcessingResult,
|
|
127
127
|
} from "./presentation/hooks/useCreationPersistence";
|
|
128
|
+
export { useProcessingJobsPoller } from "./presentation/hooks/useProcessingJobsPoller";
|
|
129
|
+
export type {
|
|
130
|
+
UseProcessingJobsPollerConfig,
|
|
131
|
+
UseProcessingJobsPollerReturn,
|
|
132
|
+
} from "./presentation/hooks/useProcessingJobsPoller";
|
|
128
133
|
|
|
129
134
|
// =============================================================================
|
|
130
135
|
// PRESENTATION LAYER - Components
|
|
@@ -8,6 +8,7 @@ declare const __DEV__: boolean;
|
|
|
8
8
|
const UPDATABLE_FIELDS = [
|
|
9
9
|
"metadata", "isShared", "uri", "type", "prompt", "status",
|
|
10
10
|
"output", "rating", "ratedAt", "isFavorite", "deletedAt",
|
|
11
|
+
"requestId", "model",
|
|
11
12
|
] as const;
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -34,6 +35,8 @@ export class CreationsWriter {
|
|
|
34
35
|
...(creation.status !== undefined && { status: creation.status }),
|
|
35
36
|
...(creation.output !== undefined && { output: creation.output }),
|
|
36
37
|
...(creation.prompt !== undefined && { prompt: creation.prompt }),
|
|
38
|
+
...(creation.requestId !== undefined && { requestId: creation.requestId }),
|
|
39
|
+
...(creation.model !== undefined && { model: creation.model }),
|
|
37
40
|
};
|
|
38
41
|
|
|
39
42
|
try {
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useProcessingJobsPoller Hook
|
|
3
|
+
* Polls FAL queue status for "processing" creations and updates Firestore when complete
|
|
4
|
+
* Enables true background generation - works even after wizard is dismissed
|
|
5
|
+
* Uses provider registry internally - no need to pass FAL functions
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useEffect, useRef, useCallback } from "react";
|
|
9
|
+
import { providerRegistry } from "../../../../infrastructure/services/provider-registry.service";
|
|
10
|
+
import type { Creation } from "../../domain/entities/Creation";
|
|
11
|
+
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
12
|
+
|
|
13
|
+
declare const __DEV__: boolean;
|
|
14
|
+
|
|
15
|
+
const POLL_INTERVAL_MS = 5000; // Poll every 5 seconds
|
|
16
|
+
|
|
17
|
+
export interface UseProcessingJobsPollerConfig {
|
|
18
|
+
readonly userId?: string | null;
|
|
19
|
+
readonly creations: Creation[];
|
|
20
|
+
readonly repository: ICreationsRepository;
|
|
21
|
+
readonly enabled?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface UseProcessingJobsPollerReturn {
|
|
25
|
+
readonly processingCount: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface FalResult {
|
|
29
|
+
video?: { url?: string };
|
|
30
|
+
output?: string;
|
|
31
|
+
images?: Array<{ url?: string }>;
|
|
32
|
+
image?: { url?: string };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function extractResultUrl(result: FalResult): { imageUrl?: string; videoUrl?: string } {
|
|
36
|
+
// Video result
|
|
37
|
+
if (result.video?.url) {
|
|
38
|
+
return { videoUrl: result.video.url };
|
|
39
|
+
}
|
|
40
|
+
// Output URL (some models)
|
|
41
|
+
if (typeof result.output === "string" && result.output.startsWith("http")) {
|
|
42
|
+
if (result.output.includes(".mp4") || result.output.includes("video")) {
|
|
43
|
+
return { videoUrl: result.output };
|
|
44
|
+
}
|
|
45
|
+
return { imageUrl: result.output };
|
|
46
|
+
}
|
|
47
|
+
// Images array
|
|
48
|
+
if (result.images?.[0]?.url) {
|
|
49
|
+
return { imageUrl: result.images[0].url };
|
|
50
|
+
}
|
|
51
|
+
// Single image
|
|
52
|
+
if (result.image?.url) {
|
|
53
|
+
return { imageUrl: result.image.url };
|
|
54
|
+
}
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function useProcessingJobsPoller(
|
|
59
|
+
config: UseProcessingJobsPollerConfig,
|
|
60
|
+
): UseProcessingJobsPollerReturn {
|
|
61
|
+
const {
|
|
62
|
+
userId,
|
|
63
|
+
creations,
|
|
64
|
+
repository,
|
|
65
|
+
enabled = true,
|
|
66
|
+
} = config;
|
|
67
|
+
|
|
68
|
+
const pollingRef = useRef<Set<string>>(new Set());
|
|
69
|
+
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
70
|
+
|
|
71
|
+
// Find creations that need polling
|
|
72
|
+
const processingJobs = creations.filter(
|
|
73
|
+
(c) => c.status === "processing" && c.requestId && c.model,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const pollJob = useCallback(
|
|
77
|
+
async (creation: Creation) => {
|
|
78
|
+
if (!userId || !creation.requestId || !creation.model) return;
|
|
79
|
+
if (pollingRef.current.has(creation.id)) return;
|
|
80
|
+
|
|
81
|
+
const provider = providerRegistry.getActiveProvider();
|
|
82
|
+
if (!provider || !provider.isInitialized()) return;
|
|
83
|
+
|
|
84
|
+
pollingRef.current.add(creation.id);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
88
|
+
console.log("[ProcessingJobsPoller] Checking status:", creation.id);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const status = await provider.getJobStatus(creation.model, creation.requestId);
|
|
92
|
+
|
|
93
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
94
|
+
console.log("[ProcessingJobsPoller] Status:", creation.id, status.status);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (status.status === "COMPLETED") {
|
|
98
|
+
// Fetch the result
|
|
99
|
+
const result = await provider.getJobResult<FalResult>(creation.model, creation.requestId);
|
|
100
|
+
const urls = extractResultUrl(result);
|
|
101
|
+
|
|
102
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
103
|
+
console.log("[ProcessingJobsPoller] Completed:", creation.id, urls);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Update Firestore
|
|
107
|
+
const uri = urls.videoUrl || urls.imageUrl || "";
|
|
108
|
+
await repository.update(userId, creation.id, {
|
|
109
|
+
status: "completed",
|
|
110
|
+
uri,
|
|
111
|
+
output: urls,
|
|
112
|
+
});
|
|
113
|
+
} else if (status.status === "FAILED") {
|
|
114
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
115
|
+
console.log("[ProcessingJobsPoller] Failed:", creation.id);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await repository.update(userId, creation.id, {
|
|
119
|
+
status: "failed",
|
|
120
|
+
metadata: { error: "Generation failed" },
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
// If still IN_PROGRESS or IN_QUEUE, we'll check again next interval
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
126
|
+
console.error("[ProcessingJobsPoller] Poll error:", creation.id, error);
|
|
127
|
+
}
|
|
128
|
+
} finally {
|
|
129
|
+
pollingRef.current.delete(creation.id);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
[userId, repository],
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (!enabled || !userId || processingJobs.length === 0) {
|
|
137
|
+
if (intervalRef.current) {
|
|
138
|
+
clearInterval(intervalRef.current);
|
|
139
|
+
intervalRef.current = null;
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Initial poll
|
|
145
|
+
processingJobs.forEach((job) => void pollJob(job));
|
|
146
|
+
|
|
147
|
+
// Set up interval polling
|
|
148
|
+
intervalRef.current = setInterval(() => {
|
|
149
|
+
processingJobs.forEach((job) => void pollJob(job));
|
|
150
|
+
}, POLL_INTERVAL_MS);
|
|
151
|
+
|
|
152
|
+
return () => {
|
|
153
|
+
if (intervalRef.current) {
|
|
154
|
+
clearInterval(intervalRef.current);
|
|
155
|
+
intervalRef.current = null;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}, [enabled, userId, processingJobs, pollJob]);
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
processingCount: processingJobs.length,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "@umituz/react-native-design-system";
|
|
9
9
|
import { useCreations } from "../hooks/useCreations";
|
|
10
10
|
import { useDeleteCreation } from "../hooks/useDeleteCreation";
|
|
11
|
+
import { useProcessingJobsPoller } from "../hooks/useProcessingJobsPoller";
|
|
11
12
|
import { useGalleryFilters } from "../hooks/useGalleryFilters";
|
|
12
13
|
import { useGalleryCallbacks } from "../hooks/useGalleryCallbacks";
|
|
13
14
|
import { GalleryHeader, CreationCard, GalleryEmptyStates } from "../components";
|
|
@@ -38,6 +39,14 @@ export function CreationsGalleryScreen({
|
|
|
38
39
|
const { data: creations, isLoading, refetch } = useCreations({ userId, repository });
|
|
39
40
|
const deleteMutation = useDeleteCreation({ userId, repository });
|
|
40
41
|
|
|
42
|
+
// Poll FAL queue for "processing" creations (enables true background generation)
|
|
43
|
+
useProcessingJobsPoller({
|
|
44
|
+
userId,
|
|
45
|
+
creations: creations ?? [],
|
|
46
|
+
repository,
|
|
47
|
+
enabled: !!userId && (creations?.length ?? 0) > 0,
|
|
48
|
+
});
|
|
49
|
+
|
|
41
50
|
useEffect(() => {
|
|
42
51
|
if (initialCreationId && creations && creations.length > 0 && !hasAutoSelectedRef.current) {
|
|
43
52
|
const creation = creations.find((c) => c.id === initialCreationId);
|
package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
* Handles video-specific generation logic (execution only)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
executeVideoFeature,
|
|
8
|
+
submitVideoFeatureToQueue,
|
|
9
|
+
} from "../../../../../infrastructure/services/video-feature-executor.service";
|
|
7
10
|
import { buildUnifiedPrompt } from "./shared/unified-prompt-builder";
|
|
8
11
|
import type { WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
|
|
9
12
|
import type { WizardStrategy } from "./wizard-strategy.types";
|
|
@@ -40,6 +43,10 @@ export async function buildVideoInput(
|
|
|
40
43
|
if (defaultPrompt) {
|
|
41
44
|
basePrompt = defaultPrompt;
|
|
42
45
|
} else {
|
|
46
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
47
|
+
console.error("[VideoStrategy] No prompt found for scenario:", scenario.id);
|
|
48
|
+
console.error("[VideoStrategy] Available defaults:", Object.keys(VIDEO_PROCESSING_PROMPTS));
|
|
49
|
+
}
|
|
43
50
|
throw new Error("error.generation.promptRequired");
|
|
44
51
|
}
|
|
45
52
|
}
|
|
@@ -95,6 +102,28 @@ export function createVideoStrategy(options: CreateVideoStrategyOptions): Wizard
|
|
|
95
102
|
return { videoUrl: result.videoUrl };
|
|
96
103
|
},
|
|
97
104
|
|
|
105
|
+
submitToQueue: async (input: unknown) => {
|
|
106
|
+
const videoInput = input as WizardVideoInput;
|
|
107
|
+
|
|
108
|
+
const result = await submitVideoFeatureToQueue(videoFeatureType, {
|
|
109
|
+
sourceImageBase64: videoInput.sourceImageBase64,
|
|
110
|
+
targetImageBase64: videoInput.targetImageBase64,
|
|
111
|
+
prompt: videoInput.prompt,
|
|
112
|
+
options: {
|
|
113
|
+
duration: videoInput.duration,
|
|
114
|
+
aspect_ratio: videoInput.aspectRatio,
|
|
115
|
+
resolution: videoInput.resolution,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
success: result.success,
|
|
121
|
+
requestId: result.requestId,
|
|
122
|
+
model: result.model,
|
|
123
|
+
error: result.error,
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
|
|
98
127
|
getCreditCost: () => creditCost,
|
|
99
128
|
};
|
|
100
129
|
}
|
package/src/domains/generation/wizard/infrastructure/strategies/wizard-strategy.constants.ts
CHANGED
|
@@ -50,4 +50,7 @@ export const VIDEO_PROCESSING_PROMPTS: Record<string, string> = {
|
|
|
50
50
|
"ai-kiss": "Create a romantic video where these two people share a gentle, loving kiss",
|
|
51
51
|
"ai-hug": "Create a heartwarming video where these two people share a warm, affectionate hug",
|
|
52
52
|
"image-to-video": "Animate this image with natural, smooth motion while preserving the original style",
|
|
53
|
+
"solo_renaissance_portrait": "Transform this person into an elegant Renaissance-style animated portrait with classical artistic movements and period-appropriate lighting",
|
|
54
|
+
"renaissance_portrait": "Transform this portrait into a majestic Renaissance-style animated painting with subtle classical movements",
|
|
55
|
+
"historical_portrait": "Animate this portrait in a historical style with period-appropriate subtle movements",
|
|
53
56
|
};
|
|
@@ -3,9 +3,18 @@
|
|
|
3
3
|
* Shared types for all wizard strategies
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
export interface QueueSubmissionResult {
|
|
7
|
+
readonly success: boolean;
|
|
8
|
+
readonly requestId?: string;
|
|
9
|
+
readonly model?: string;
|
|
10
|
+
readonly error?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
export interface WizardStrategy {
|
|
7
|
-
/** Execute the generation - returns result with URLs */
|
|
14
|
+
/** Execute the generation - returns result with URLs (blocking) */
|
|
8
15
|
execute: (input: unknown) => Promise<{ imageUrl?: string; videoUrl?: string }>;
|
|
16
|
+
/** Submit to queue for background processing - returns immediately with requestId */
|
|
17
|
+
submitToQueue?: (input: unknown) => Promise<QueueSubmissionResult>;
|
|
9
18
|
/** Get credit cost for this generation */
|
|
10
19
|
getCreditCost: () => number;
|
|
11
20
|
}
|
|
@@ -16,6 +16,9 @@ export interface ProcessingCreationData {
|
|
|
16
16
|
readonly scenarioId: string;
|
|
17
17
|
readonly scenarioTitle: string;
|
|
18
18
|
readonly prompt: string;
|
|
19
|
+
// Background job tracking - FAL queue
|
|
20
|
+
readonly requestId?: string;
|
|
21
|
+
readonly model?: string;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
export interface CompletedCreationData {
|
|
@@ -51,6 +54,8 @@ export function createCreationPersistence(config: CreationPersistenceConfig = {}
|
|
|
51
54
|
createdAt: new Date(),
|
|
52
55
|
isShared: false,
|
|
53
56
|
isFavorite: false,
|
|
57
|
+
requestId: data.requestId,
|
|
58
|
+
model: data.model,
|
|
54
59
|
metadata: {
|
|
55
60
|
scenarioId: data.scenarioId,
|
|
56
61
|
scenarioTitle: data.scenarioTitle,
|
|
@@ -58,7 +63,11 @@ export function createCreationPersistence(config: CreationPersistenceConfig = {}
|
|
|
58
63
|
});
|
|
59
64
|
|
|
60
65
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
61
|
-
console.log("[CreationPersistence] Saved as processing", {
|
|
66
|
+
console.log("[CreationPersistence] Saved as processing", {
|
|
67
|
+
creationId,
|
|
68
|
+
requestId: data.requestId,
|
|
69
|
+
model: data.model,
|
|
70
|
+
});
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
return creationId;
|
|
@@ -104,6 +113,26 @@ export function createCreationPersistence(config: CreationPersistenceConfig = {}
|
|
|
104
113
|
console.log("[CreationPersistence] Updated to failed", { creationId, error });
|
|
105
114
|
}
|
|
106
115
|
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Update creation with FAL queue requestId and model after job submission
|
|
119
|
+
* Call this after fal.queue.submit() returns
|
|
120
|
+
*/
|
|
121
|
+
updateRequestId: async (
|
|
122
|
+
userId: string,
|
|
123
|
+
creationId: string,
|
|
124
|
+
requestId: string,
|
|
125
|
+
model: string,
|
|
126
|
+
): Promise<void> => {
|
|
127
|
+
await repository.update(userId, creationId, {
|
|
128
|
+
requestId,
|
|
129
|
+
model,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
133
|
+
console.log("[CreationPersistence] Updated requestId", { creationId, requestId, model });
|
|
134
|
+
}
|
|
135
|
+
},
|
|
107
136
|
};
|
|
108
137
|
}
|
|
109
138
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generation Result Utilities
|
|
3
|
+
* Shared utilities for extracting and processing generation results
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface FalResult {
|
|
7
|
+
video?: { url?: string };
|
|
8
|
+
output?: string;
|
|
9
|
+
images?: Array<{ url?: string }>;
|
|
10
|
+
image?: { url?: string };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface GenerationUrls {
|
|
14
|
+
imageUrl?: string;
|
|
15
|
+
videoUrl?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extracts image/video URL from FAL result
|
|
20
|
+
* Handles various result formats from different FAL models
|
|
21
|
+
*/
|
|
22
|
+
export function extractResultUrl(result: FalResult): GenerationUrls {
|
|
23
|
+
// Video result
|
|
24
|
+
if (result.video?.url) {
|
|
25
|
+
return { videoUrl: result.video.url };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Output URL (some models return direct URL)
|
|
29
|
+
if (typeof result.output === "string" && result.output.startsWith("http")) {
|
|
30
|
+
if (result.output.includes(".mp4") || result.output.includes("video")) {
|
|
31
|
+
return { videoUrl: result.output };
|
|
32
|
+
}
|
|
33
|
+
return { imageUrl: result.output };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Images array (most image models)
|
|
37
|
+
if (result.images?.[0]?.url) {
|
|
38
|
+
return { imageUrl: result.images[0].url };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Single image
|
|
42
|
+
if (result.image?.url) {
|
|
43
|
+
return { imageUrl: result.image.url };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePhotoBlockingGeneration Hook
|
|
3
|
+
* Handles photo generation via blocking execution
|
|
4
|
+
* - Uses orchestrator for synchronous generation
|
|
5
|
+
* - Waits for result before returning
|
|
6
|
+
* - Best for quick image operations (10-30 seconds)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useRef, useCallback } from "react";
|
|
10
|
+
import { useGenerationOrchestrator } from "../../../../../presentation/hooks/generation";
|
|
11
|
+
import type { CreationPersistence } from "../../infrastructure/utils/creation-persistence.util";
|
|
12
|
+
import type { WizardStrategy } from "../../infrastructure/strategies/wizard-strategy.types";
|
|
13
|
+
import type { WizardScenarioData } from "./wizard-generation.types";
|
|
14
|
+
import type { AlertMessages } from "../../../../../presentation/hooks/generation/types";
|
|
15
|
+
|
|
16
|
+
declare const __DEV__: boolean;
|
|
17
|
+
|
|
18
|
+
export interface UsePhotoBlockingGenerationProps {
|
|
19
|
+
readonly userId?: string;
|
|
20
|
+
readonly scenario: WizardScenarioData;
|
|
21
|
+
readonly persistence: CreationPersistence;
|
|
22
|
+
readonly strategy: WizardStrategy;
|
|
23
|
+
readonly alertMessages: AlertMessages;
|
|
24
|
+
readonly onSuccess?: (result: unknown) => void;
|
|
25
|
+
readonly onError?: (error: string) => void;
|
|
26
|
+
readonly onCreditsExhausted?: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface UsePhotoBlockingGenerationReturn {
|
|
30
|
+
readonly isGenerating: boolean;
|
|
31
|
+
readonly startGeneration: (input: unknown, prompt: string) => Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function usePhotoBlockingGeneration(
|
|
35
|
+
props: UsePhotoBlockingGenerationProps,
|
|
36
|
+
): UsePhotoBlockingGenerationReturn {
|
|
37
|
+
const {
|
|
38
|
+
userId,
|
|
39
|
+
scenario,
|
|
40
|
+
persistence,
|
|
41
|
+
strategy,
|
|
42
|
+
alertMessages,
|
|
43
|
+
onSuccess,
|
|
44
|
+
onError,
|
|
45
|
+
onCreditsExhausted,
|
|
46
|
+
} = props;
|
|
47
|
+
|
|
48
|
+
const creationIdRef = useRef<string | null>(null);
|
|
49
|
+
|
|
50
|
+
const handleSuccess = useCallback(
|
|
51
|
+
async (result: unknown) => {
|
|
52
|
+
const typedResult = result as { imageUrl?: string; videoUrl?: string };
|
|
53
|
+
const creationId = creationIdRef.current;
|
|
54
|
+
|
|
55
|
+
if (creationId && userId) {
|
|
56
|
+
try {
|
|
57
|
+
await persistence.updateToCompleted(userId, creationId, {
|
|
58
|
+
uri: typedResult.imageUrl || typedResult.videoUrl || "",
|
|
59
|
+
imageUrl: typedResult.imageUrl,
|
|
60
|
+
videoUrl: typedResult.videoUrl,
|
|
61
|
+
});
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
64
|
+
console.error("[PhotoBlockingGeneration] updateToCompleted error:", err);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
creationIdRef.current = null;
|
|
70
|
+
onSuccess?.(result);
|
|
71
|
+
},
|
|
72
|
+
[userId, persistence, onSuccess],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const handleError = useCallback(
|
|
76
|
+
async (err: { message: string }) => {
|
|
77
|
+
const creationId = creationIdRef.current;
|
|
78
|
+
|
|
79
|
+
if (creationId && userId) {
|
|
80
|
+
try {
|
|
81
|
+
await persistence.updateToFailed(userId, creationId, err.message);
|
|
82
|
+
} catch (updateErr) {
|
|
83
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
84
|
+
console.error("[PhotoBlockingGeneration] updateToFailed error:", updateErr);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
creationIdRef.current = null;
|
|
90
|
+
onError?.(err.message);
|
|
91
|
+
},
|
|
92
|
+
[userId, persistence, onError],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const { generate, isGenerating } = useGenerationOrchestrator(strategy, {
|
|
96
|
+
userId,
|
|
97
|
+
alertMessages,
|
|
98
|
+
onCreditsExhausted,
|
|
99
|
+
onSuccess: handleSuccess,
|
|
100
|
+
onError: handleError,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const startGeneration = useCallback(
|
|
104
|
+
async (input: unknown, prompt: string) => {
|
|
105
|
+
// Save to Firestore first
|
|
106
|
+
if (userId && prompt) {
|
|
107
|
+
try {
|
|
108
|
+
const creationId = await persistence.saveAsProcessing(userId, {
|
|
109
|
+
scenarioId: scenario.id,
|
|
110
|
+
scenarioTitle: scenario.title || scenario.id,
|
|
111
|
+
prompt,
|
|
112
|
+
});
|
|
113
|
+
creationIdRef.current = creationId;
|
|
114
|
+
|
|
115
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
116
|
+
console.log("[PhotoBlockingGeneration] Saved as processing:", creationId);
|
|
117
|
+
}
|
|
118
|
+
} catch (err) {
|
|
119
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
120
|
+
console.error("[PhotoBlockingGeneration] saveAsProcessing error:", err);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Start blocking generation
|
|
126
|
+
generate(input);
|
|
127
|
+
},
|
|
128
|
+
[userId, scenario, persistence, generate],
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return { isGenerating, startGeneration };
|
|
132
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useVideoQueueGeneration Hook
|
|
3
|
+
* Handles video generation via FAL queue with background support
|
|
4
|
+
* - Submits to queue for non-blocking generation
|
|
5
|
+
* - Polls for completion status
|
|
6
|
+
* - Supports background generation (user can dismiss wizard)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useEffect, useRef, useCallback, useState } from "react";
|
|
10
|
+
import { providerRegistry } from "../../../../../infrastructure/services/provider-registry.service";
|
|
11
|
+
import { extractResultUrl, type FalResult, type GenerationUrls } from "./generation-result.utils";
|
|
12
|
+
import type { CreationPersistence } from "../../infrastructure/utils/creation-persistence.util";
|
|
13
|
+
import type { WizardStrategy } from "../../infrastructure/strategies/wizard-strategy.types";
|
|
14
|
+
import type { WizardScenarioData } from "./wizard-generation.types";
|
|
15
|
+
|
|
16
|
+
declare const __DEV__: boolean;
|
|
17
|
+
|
|
18
|
+
const POLL_INTERVAL_MS = 3000;
|
|
19
|
+
|
|
20
|
+
export interface UseVideoQueueGenerationProps {
|
|
21
|
+
readonly userId?: string;
|
|
22
|
+
readonly scenario: WizardScenarioData;
|
|
23
|
+
readonly persistence: CreationPersistence;
|
|
24
|
+
readonly strategy: WizardStrategy;
|
|
25
|
+
readonly onSuccess?: (result: unknown) => void;
|
|
26
|
+
readonly onError?: (error: string) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface UseVideoQueueGenerationReturn {
|
|
30
|
+
readonly isGenerating: boolean;
|
|
31
|
+
readonly startGeneration: (input: unknown, prompt: string) => Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useVideoQueueGeneration(
|
|
35
|
+
props: UseVideoQueueGenerationProps,
|
|
36
|
+
): UseVideoQueueGenerationReturn {
|
|
37
|
+
const { userId, scenario, persistence, strategy, onSuccess, onError } = props;
|
|
38
|
+
|
|
39
|
+
const creationIdRef = useRef<string | null>(null);
|
|
40
|
+
const requestIdRef = useRef<string | null>(null);
|
|
41
|
+
const modelRef = useRef<string | null>(null);
|
|
42
|
+
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
43
|
+
const [isGenerating, setIsGenerating] = useState(false);
|
|
44
|
+
|
|
45
|
+
// Cleanup polling on unmount
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
return () => {
|
|
48
|
+
if (pollingRef.current) {
|
|
49
|
+
clearInterval(pollingRef.current);
|
|
50
|
+
pollingRef.current = null;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
const resetRefs = useCallback(() => {
|
|
56
|
+
creationIdRef.current = null;
|
|
57
|
+
requestIdRef.current = null;
|
|
58
|
+
modelRef.current = null;
|
|
59
|
+
setIsGenerating(false);
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
const handleComplete = useCallback(
|
|
63
|
+
async (urls: GenerationUrls) => {
|
|
64
|
+
const creationId = creationIdRef.current;
|
|
65
|
+
if (creationId && userId) {
|
|
66
|
+
try {
|
|
67
|
+
await persistence.updateToCompleted(userId, creationId, {
|
|
68
|
+
uri: urls.videoUrl || urls.imageUrl || "",
|
|
69
|
+
imageUrl: urls.imageUrl,
|
|
70
|
+
videoUrl: urls.videoUrl,
|
|
71
|
+
});
|
|
72
|
+
} catch (err) {
|
|
73
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) console.error("[VideoQueueGeneration] updateToCompleted error:", err);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
resetRefs();
|
|
77
|
+
onSuccess?.(urls);
|
|
78
|
+
},
|
|
79
|
+
[userId, persistence, onSuccess, resetRefs],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const handleError = useCallback(
|
|
83
|
+
async (errorMsg: string) => {
|
|
84
|
+
const creationId = creationIdRef.current;
|
|
85
|
+
if (creationId && userId) {
|
|
86
|
+
try {
|
|
87
|
+
await persistence.updateToFailed(userId, creationId, errorMsg);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) console.error("[VideoQueueGeneration] updateToFailed error:", err);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
resetRefs();
|
|
93
|
+
onError?.(errorMsg);
|
|
94
|
+
},
|
|
95
|
+
[userId, persistence, onError, resetRefs],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const pollQueueStatus = useCallback(async () => {
|
|
99
|
+
const requestId = requestIdRef.current;
|
|
100
|
+
const model = modelRef.current;
|
|
101
|
+
const provider = providerRegistry.getActiveProvider();
|
|
102
|
+
if (!requestId || !model || !provider) return;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const status = await provider.getJobStatus(model, requestId);
|
|
106
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[VideoQueueGeneration] Poll:", status.status);
|
|
107
|
+
|
|
108
|
+
if (status.status === "COMPLETED" || status.status === "FAILED") {
|
|
109
|
+
if (pollingRef.current) { clearInterval(pollingRef.current); pollingRef.current = null; }
|
|
110
|
+
if (status.status === "COMPLETED") {
|
|
111
|
+
const result = await provider.getJobResult<FalResult>(model, requestId);
|
|
112
|
+
await handleComplete(extractResultUrl(result));
|
|
113
|
+
} else {
|
|
114
|
+
await handleError("Generation failed");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) console.error("[VideoQueueGeneration] Poll error:", err);
|
|
119
|
+
}
|
|
120
|
+
}, [handleComplete, handleError]);
|
|
121
|
+
|
|
122
|
+
const startGeneration = useCallback(
|
|
123
|
+
async (input: unknown, prompt: string) => {
|
|
124
|
+
if (!strategy.submitToQueue) { onError?.("Queue submission not available"); return; }
|
|
125
|
+
setIsGenerating(true);
|
|
126
|
+
|
|
127
|
+
// Save to Firestore FIRST (enables background visibility)
|
|
128
|
+
let creationId: string | null = null;
|
|
129
|
+
if (userId && prompt) {
|
|
130
|
+
try {
|
|
131
|
+
creationId = await persistence.saveAsProcessing(userId, {
|
|
132
|
+
scenarioId: scenario.id, scenarioTitle: scenario.title || scenario.id, prompt,
|
|
133
|
+
});
|
|
134
|
+
creationIdRef.current = creationId;
|
|
135
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[VideoQueueGeneration] Saved:", creationId);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) console.error("[VideoQueueGeneration] save error:", err);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const queueResult = await strategy.submitToQueue(input);
|
|
142
|
+
if (!queueResult.success || !queueResult.requestId || !queueResult.model) {
|
|
143
|
+
if (creationId && userId) await persistence.updateToFailed(userId, creationId, queueResult.error || "Queue submission failed");
|
|
144
|
+
setIsGenerating(false);
|
|
145
|
+
onError?.(queueResult.error || "Queue submission failed");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
requestIdRef.current = queueResult.requestId;
|
|
150
|
+
modelRef.current = queueResult.model;
|
|
151
|
+
|
|
152
|
+
// Update with requestId for background polling
|
|
153
|
+
if (creationId && userId) {
|
|
154
|
+
try {
|
|
155
|
+
await persistence.updateRequestId(userId, creationId, queueResult.requestId, queueResult.model);
|
|
156
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[VideoQueueGeneration] Updated requestId:", queueResult.requestId);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) console.error("[VideoQueueGeneration] updateRequestId error:", err);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
pollingRef.current = setInterval(() => void pollQueueStatus(), POLL_INTERVAL_MS);
|
|
163
|
+
void pollQueueStatus();
|
|
164
|
+
},
|
|
165
|
+
[userId, scenario, persistence, strategy, pollQueueStatus, onError],
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
return { isGenerating, startGeneration };
|
|
169
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useWizardGeneration Hook
|
|
3
|
-
*
|
|
4
|
-
* -
|
|
5
|
-
* -
|
|
6
|
-
* - Updates to status="failed" on error
|
|
3
|
+
* Orchestrates wizard-based generation by delegating to appropriate mode:
|
|
4
|
+
* - Video: Queue-based generation with background support
|
|
5
|
+
* - Photo: Blocking execution for quick results
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
|
-
import { useEffect, useRef, useMemo
|
|
10
|
-
import { useGenerationOrchestrator } from "../../../../../presentation/hooks/generation";
|
|
8
|
+
import { useEffect, useRef, useMemo } from "react";
|
|
11
9
|
import { createWizardStrategy, buildWizardInput } from "../../infrastructure/strategies";
|
|
12
10
|
import { createCreationPersistence } from "../../infrastructure/utils/creation-persistence.util";
|
|
11
|
+
import { useVideoQueueGeneration } from "./useVideoQueueGeneration";
|
|
12
|
+
import { usePhotoBlockingGeneration } from "./usePhotoBlockingGeneration";
|
|
13
13
|
import type {
|
|
14
14
|
UseWizardGenerationProps,
|
|
15
15
|
UseWizardGenerationReturn,
|
|
@@ -40,77 +40,42 @@ export const useWizardGeneration = (
|
|
|
40
40
|
} = props;
|
|
41
41
|
|
|
42
42
|
const hasStarted = useRef(false);
|
|
43
|
-
const creationIdRef = useRef<string | null>(null);
|
|
44
|
-
const inputRef = useRef<{ prompt: string } | null>(null);
|
|
45
43
|
|
|
46
|
-
// Persistence utility - separate from strategy
|
|
47
44
|
const persistence = useMemo(() => createCreationPersistence(), []);
|
|
48
|
-
|
|
49
|
-
// Strategy - only handles execution, creditCost is passed from app
|
|
50
45
|
const strategy = useMemo(
|
|
51
46
|
() => createWizardStrategy({ scenario, creditCost }),
|
|
52
47
|
[scenario, creditCost],
|
|
53
48
|
);
|
|
54
49
|
|
|
55
|
-
const
|
|
56
|
-
async (result: unknown) => {
|
|
57
|
-
const typedResult = result as { imageUrl?: string; videoUrl?: string };
|
|
58
|
-
const creationId = creationIdRef.current;
|
|
59
|
-
|
|
60
|
-
// Update to completed in Firestore
|
|
61
|
-
if (creationId && userId) {
|
|
62
|
-
try {
|
|
63
|
-
await persistence.updateToCompleted(userId, creationId, {
|
|
64
|
-
uri: typedResult.imageUrl || typedResult.videoUrl || "",
|
|
65
|
-
imageUrl: typedResult.imageUrl,
|
|
66
|
-
videoUrl: typedResult.videoUrl,
|
|
67
|
-
});
|
|
68
|
-
} catch (err) {
|
|
69
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
70
|
-
console.error("[useWizardGeneration] updateToCompleted error:", err);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
creationIdRef.current = null;
|
|
76
|
-
inputRef.current = null;
|
|
77
|
-
onSuccess?.(result);
|
|
78
|
-
},
|
|
79
|
-
[userId, persistence, onSuccess],
|
|
80
|
-
);
|
|
50
|
+
const isVideoMode = scenario.outputType === "video" && !!strategy.submitToQueue;
|
|
81
51
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
92
|
-
console.error("[useWizardGeneration] updateToFailed error:", updateErr);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
creationIdRef.current = null;
|
|
98
|
-
inputRef.current = null;
|
|
99
|
-
onError?.(err.message);
|
|
100
|
-
},
|
|
101
|
-
[userId, persistence, onError],
|
|
102
|
-
);
|
|
52
|
+
// Video generation hook (queue-based)
|
|
53
|
+
const videoGeneration = useVideoQueueGeneration({
|
|
54
|
+
userId,
|
|
55
|
+
scenario,
|
|
56
|
+
persistence,
|
|
57
|
+
strategy,
|
|
58
|
+
onSuccess,
|
|
59
|
+
onError,
|
|
60
|
+
});
|
|
103
61
|
|
|
104
|
-
|
|
62
|
+
// Photo generation hook (blocking)
|
|
63
|
+
const photoGeneration = usePhotoBlockingGeneration({
|
|
105
64
|
userId,
|
|
65
|
+
scenario,
|
|
66
|
+
persistence,
|
|
67
|
+
strategy,
|
|
106
68
|
alertMessages,
|
|
69
|
+
onSuccess,
|
|
70
|
+
onError,
|
|
107
71
|
onCreditsExhausted,
|
|
108
|
-
onSuccess: handleSuccess,
|
|
109
|
-
onError: handleError,
|
|
110
72
|
});
|
|
111
73
|
|
|
74
|
+
// Main effect: trigger generation when step becomes active
|
|
112
75
|
useEffect(() => {
|
|
113
|
-
|
|
76
|
+
const isAlreadyGenerating = videoGeneration.isGenerating || photoGeneration.isGenerating;
|
|
77
|
+
|
|
78
|
+
if (isGeneratingStep && !hasStarted.current && !isAlreadyGenerating) {
|
|
114
79
|
hasStarted.current = true;
|
|
115
80
|
|
|
116
81
|
buildWizardInput(wizardData, scenario)
|
|
@@ -121,32 +86,22 @@ export const useWizardGeneration = (
|
|
|
121
86
|
return;
|
|
122
87
|
}
|
|
123
88
|
|
|
124
|
-
inputRef.current = input as { prompt: string };
|
|
125
|
-
|
|
126
|
-
// Save to Firestore with status="processing"
|
|
127
89
|
const typedInput = input as { prompt?: string };
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
scenarioId: scenario.id,
|
|
132
|
-
scenarioTitle: scenario.title || scenario.id,
|
|
133
|
-
prompt: typedInput.prompt,
|
|
134
|
-
});
|
|
135
|
-
creationIdRef.current = creationId;
|
|
136
|
-
|
|
137
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
138
|
-
console.log("[useWizardGeneration] Saved as processing:", creationId);
|
|
139
|
-
}
|
|
140
|
-
} catch (err) {
|
|
141
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
142
|
-
console.error("[useWizardGeneration] saveAsProcessing error:", err);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
90
|
+
|
|
91
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
92
|
+
console.log("[WizardGeneration] Mode:", isVideoMode ? "VIDEO_QUEUE" : "PHOTO_BLOCKING");
|
|
145
93
|
}
|
|
146
94
|
|
|
147
|
-
|
|
95
|
+
if (isVideoMode) {
|
|
96
|
+
await videoGeneration.startGeneration(input, typedInput.prompt || "");
|
|
97
|
+
} else {
|
|
98
|
+
await photoGeneration.startGeneration(input, typedInput.prompt || "");
|
|
99
|
+
}
|
|
148
100
|
})
|
|
149
101
|
.catch((error) => {
|
|
102
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
103
|
+
console.error("[WizardGeneration] Build input error:", error.message);
|
|
104
|
+
}
|
|
150
105
|
hasStarted.current = false;
|
|
151
106
|
onError?.(error.message);
|
|
152
107
|
});
|
|
@@ -155,7 +110,17 @@ export const useWizardGeneration = (
|
|
|
155
110
|
if (!isGeneratingStep && hasStarted.current) {
|
|
156
111
|
hasStarted.current = false;
|
|
157
112
|
}
|
|
158
|
-
}, [
|
|
113
|
+
}, [
|
|
114
|
+
isGeneratingStep,
|
|
115
|
+
scenario,
|
|
116
|
+
wizardData,
|
|
117
|
+
isVideoMode,
|
|
118
|
+
videoGeneration,
|
|
119
|
+
photoGeneration,
|
|
120
|
+
onError,
|
|
121
|
+
]);
|
|
159
122
|
|
|
160
|
-
return {
|
|
123
|
+
return {
|
|
124
|
+
isGenerating: videoGeneration.isGenerating || photoGeneration.isGenerating,
|
|
125
|
+
};
|
|
161
126
|
};
|
package/src/index.ts
CHANGED
|
@@ -47,6 +47,7 @@ export {
|
|
|
47
47
|
export {
|
|
48
48
|
providerRegistry, generationOrchestrator, pollJob, createJobPoller,
|
|
49
49
|
executeImageFeature, hasImageFeatureSupport, executeVideoFeature, hasVideoFeatureSupport,
|
|
50
|
+
submitVideoFeatureToQueue,
|
|
50
51
|
} from "./infrastructure/services";
|
|
51
52
|
|
|
52
53
|
export type {
|
|
@@ -160,6 +160,67 @@ export function hasVideoFeatureSupport(): boolean {
|
|
|
160
160
|
return provider !== null && provider.isInitialized();
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Submit a video feature to the queue for background processing
|
|
165
|
+
* Returns immediately with requestId and model for later status polling
|
|
166
|
+
*/
|
|
167
|
+
export async function submitVideoFeatureToQueue(
|
|
168
|
+
featureType: VideoFeatureType,
|
|
169
|
+
request: VideoFeatureRequest,
|
|
170
|
+
): Promise<{ success: boolean; requestId?: string; model?: string; error?: string }> {
|
|
171
|
+
if (__DEV__) {
|
|
172
|
+
console.log(`[VideoExecutor:${featureType}] QUEUE SUBMIT`, {
|
|
173
|
+
hasSource: !!request.sourceImageBase64,
|
|
174
|
+
hasTarget: !!request.targetImageBase64,
|
|
175
|
+
promptLength: request.prompt?.length ?? 0,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const provider = providerRegistry.getActiveProvider();
|
|
180
|
+
|
|
181
|
+
if (!provider) {
|
|
182
|
+
return { success: false, error: "No AI provider configured" };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!provider.isInitialized()) {
|
|
186
|
+
return { success: false, error: "AI provider not initialized" };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const model = provider.getVideoFeatureModel(featureType);
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const inputData: VideoFeatureInputData = {
|
|
193
|
+
sourceImageBase64: cleanBase64(request.sourceImageBase64),
|
|
194
|
+
targetImageBase64: cleanBase64(request.targetImageBase64),
|
|
195
|
+
prompt: request.prompt,
|
|
196
|
+
options: request.options,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const input = provider.buildVideoFeatureInput(featureType, inputData);
|
|
200
|
+
|
|
201
|
+
const submission = await provider.submitJob(model, input);
|
|
202
|
+
|
|
203
|
+
if (__DEV__) {
|
|
204
|
+
console.log(`[VideoExecutor:${featureType}] QUEUE SUBMITTED`, {
|
|
205
|
+
requestId: submission.requestId,
|
|
206
|
+
model,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
success: true,
|
|
212
|
+
requestId: submission.requestId,
|
|
213
|
+
model,
|
|
214
|
+
};
|
|
215
|
+
} catch (error) {
|
|
216
|
+
const message = extractErrorMessage(error, "Queue submission failed", `Video:${featureType}`);
|
|
217
|
+
if (__DEV__) {
|
|
218
|
+
console.error(`[VideoExecutor:${featureType}] QUEUE EXCEPTION`, { error: message });
|
|
219
|
+
}
|
|
220
|
+
return { success: false, error: message };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
163
224
|
export type {
|
|
164
225
|
ExecuteVideoFeatureOptions,
|
|
165
226
|
VideoFeatureResult,
|