@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.48.2",
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);
@@ -3,7 +3,10 @@
3
3
  * Handles video-specific generation logic (execution only)
4
4
  */
5
5
 
6
- import { executeVideoFeature } from "../../../../../infrastructure/services/video-feature-executor.service";
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
  }
@@ -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", { creationId });
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
- * Wizard generation with Firestore persistence
4
- * - Saves status="processing" at start
5
- * - Updates to status="completed" on success
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, useCallback } from "react";
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 handleSuccess = useCallback(
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
- const handleError = useCallback(
83
- async (err: { message: string }) => {
84
- const creationId = creationIdRef.current;
85
-
86
- // Update to failed in Firestore
87
- if (creationId && userId) {
88
- try {
89
- await persistence.updateToFailed(userId, creationId, err.message);
90
- } catch (updateErr) {
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
- const { generate, isGenerating } = useGenerationOrchestrator(strategy, {
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
- if (isGeneratingStep && !hasStarted.current && !isGenerating) {
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
- if (userId && typedInput.prompt) {
129
- try {
130
- const creationId = await persistence.saveAsProcessing(userId, {
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
- generate(input);
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
- }, [isGeneratingStep, scenario, wizardData, isGenerating, generate, onError, userId, persistence]);
113
+ }, [
114
+ isGeneratingStep,
115
+ scenario,
116
+ wizardData,
117
+ isVideoMode,
118
+ videoGeneration,
119
+ photoGeneration,
120
+ onError,
121
+ ]);
159
122
 
160
- return { isGenerating };
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 {
@@ -20,6 +20,7 @@ export type { ImageResultExtractor } from "../utils/url-extractor";
20
20
  export {
21
21
  executeVideoFeature,
22
22
  hasVideoFeatureSupport,
23
+ submitVideoFeatureToQueue,
23
24
  } from "./video-feature-executor.service";
24
25
  export type {
25
26
  ExecuteVideoFeatureOptions,
@@ -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,