@umituz/react-native-ai-generation-content 1.12.26 → 1.12.31
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/domain/constants/processing-modes.constants.ts +138 -0
- package/src/domain/entities/processing-modes.types.ts +27 -0
- package/src/domains/creations/presentation/components/GalleryEmptyStates.tsx +87 -0
- package/src/domains/creations/presentation/components/index.ts +1 -0
- package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +16 -39
- package/src/domains/prompts/infrastructure/services/AIServiceProcessor.ts +142 -0
- package/src/domains/prompts/presentation/hooks/useAIServices.ts +15 -132
- package/src/index.ts +18 -0
- package/src/infrastructure/services/generation-orchestrator.service.ts +25 -162
- package/src/infrastructure/services/job-poller.ts +103 -0
- package/src/infrastructure/services/progress-manager.ts +58 -0
- package/src/infrastructure/services/provider-validator.ts +52 -0
package/package.json
CHANGED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Image Processing Modes
|
|
3
|
+
* Pre-configured modes for common image processing tasks
|
|
4
|
+
* Apps can use these defaults or provide their own configurations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ImageProcessingMode, ModeCatalog, ModeConfig } from "../entities/processing-modes.types";
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_PROCESSING_MODES: ModeCatalog = {
|
|
10
|
+
clean_white: {
|
|
11
|
+
id: "clean_white",
|
|
12
|
+
icon: "square",
|
|
13
|
+
cost: 1,
|
|
14
|
+
premium: false,
|
|
15
|
+
requiresPrompt: false,
|
|
16
|
+
aiPrompt: `Replace the background with a clean, pure white background.
|
|
17
|
+
Keep the subject perfectly intact with professional edges.
|
|
18
|
+
Add subtle shadows beneath the subject for a grounded look.
|
|
19
|
+
This should look like a professional product or portrait photo.`,
|
|
20
|
+
},
|
|
21
|
+
portrait_blur: {
|
|
22
|
+
id: "portrait_blur",
|
|
23
|
+
icon: "person",
|
|
24
|
+
cost: 1,
|
|
25
|
+
premium: false,
|
|
26
|
+
requiresPrompt: false,
|
|
27
|
+
aiPrompt: `Create a professional portrait with a beautifully blurred background (bokeh effect).
|
|
28
|
+
Keep the subject in sharp focus with clean edges.
|
|
29
|
+
The background should have a soft, creamy blur similar to f/1.4 depth of field.
|
|
30
|
+
Maintain natural skin tones and lighting on the subject.`,
|
|
31
|
+
},
|
|
32
|
+
creative_scene: {
|
|
33
|
+
id: "creative_scene",
|
|
34
|
+
icon: "image",
|
|
35
|
+
cost: 2,
|
|
36
|
+
premium: false,
|
|
37
|
+
requiresPrompt: true,
|
|
38
|
+
aiPrompt: `Transform this image by placing the subject in a new creative scene.
|
|
39
|
+
The subject must remain completely unchanged - same pose, expression, clothing.
|
|
40
|
+
Seamlessly blend the subject into the new background with matched lighting.
|
|
41
|
+
Create a professional, realistic composite that looks natural.`,
|
|
42
|
+
},
|
|
43
|
+
transparent: {
|
|
44
|
+
id: "transparent",
|
|
45
|
+
icon: "remove-circle",
|
|
46
|
+
cost: 1,
|
|
47
|
+
premium: false,
|
|
48
|
+
requiresPrompt: false,
|
|
49
|
+
aiPrompt: `Remove the background completely from this image.
|
|
50
|
+
Keep the main subject perfectly intact with clean edges.
|
|
51
|
+
Output should have a transparent or solid white background.
|
|
52
|
+
Maintain all original details, colors, and lighting on the subject.`,
|
|
53
|
+
},
|
|
54
|
+
enhance: {
|
|
55
|
+
id: "enhance",
|
|
56
|
+
icon: "sparkles",
|
|
57
|
+
cost: 2,
|
|
58
|
+
premium: false,
|
|
59
|
+
requiresPrompt: false,
|
|
60
|
+
aiPrompt: `Enhance this image with professional quality improvements.
|
|
61
|
+
Improve lighting, color balance, and overall image quality.
|
|
62
|
+
Remove any imperfections while maintaining natural appearance.
|
|
63
|
+
The result should look professionally retouched but realistic.`,
|
|
64
|
+
},
|
|
65
|
+
remove_object: {
|
|
66
|
+
id: "remove_object",
|
|
67
|
+
icon: "trash",
|
|
68
|
+
cost: 2,
|
|
69
|
+
premium: true,
|
|
70
|
+
requiresPrompt: true,
|
|
71
|
+
aiPrompt: `Remove unwanted objects from this image.
|
|
72
|
+
Fill the removed areas with appropriate background content.
|
|
73
|
+
The result should look natural with no visible artifacts.
|
|
74
|
+
Maintain the overall composition and quality of the image.`,
|
|
75
|
+
},
|
|
76
|
+
replace_object: {
|
|
77
|
+
id: "replace_object",
|
|
78
|
+
icon: "swap-horizontal",
|
|
79
|
+
cost: 3,
|
|
80
|
+
premium: true,
|
|
81
|
+
requiresPrompt: true,
|
|
82
|
+
aiPrompt: `Replace the specified object in this image with a new one.
|
|
83
|
+
The replacement should blend naturally with the scene.
|
|
84
|
+
Match the lighting, perspective, and style of the original.
|
|
85
|
+
Ensure the result looks realistic and professionally edited.`,
|
|
86
|
+
},
|
|
87
|
+
relight: {
|
|
88
|
+
id: "relight",
|
|
89
|
+
icon: "sunny",
|
|
90
|
+
cost: 2,
|
|
91
|
+
premium: true,
|
|
92
|
+
requiresPrompt: true,
|
|
93
|
+
aiPrompt: `Relight this image with professional studio lighting.
|
|
94
|
+
Apply dramatic, flattering light that enhances the subject.
|
|
95
|
+
Add subtle shadows and highlights for depth and dimension.
|
|
96
|
+
The result should look like a professional studio photograph.`,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get mode configuration by ID
|
|
102
|
+
* Returns default transparent mode if not found
|
|
103
|
+
*/
|
|
104
|
+
export const getModeConfig = (
|
|
105
|
+
mode: string,
|
|
106
|
+
customModes?: ModeCatalog
|
|
107
|
+
): ModeConfig => {
|
|
108
|
+
const modes = customModes || DEFAULT_PROCESSING_MODES;
|
|
109
|
+
const key = mode.replace("-", "_") as ImageProcessingMode;
|
|
110
|
+
return modes[key] || DEFAULT_PROCESSING_MODES.transparent;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Filter modes by premium status
|
|
115
|
+
*/
|
|
116
|
+
export const getFreeModes = (modes: ModeCatalog = DEFAULT_PROCESSING_MODES): ModeCatalog => {
|
|
117
|
+
return Object.fromEntries(
|
|
118
|
+
Object.entries(modes).filter(([, config]) => !config.premium)
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Filter modes by premium status
|
|
124
|
+
*/
|
|
125
|
+
export const getPremiumModes = (modes: ModeCatalog = DEFAULT_PROCESSING_MODES): ModeCatalog => {
|
|
126
|
+
return Object.fromEntries(
|
|
127
|
+
Object.entries(modes).filter(([, config]) => config.premium)
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get modes that require custom prompts
|
|
133
|
+
*/
|
|
134
|
+
export const getPromptRequiredModes = (modes: ModeCatalog = DEFAULT_PROCESSING_MODES): ModeCatalog => {
|
|
135
|
+
return Object.fromEntries(
|
|
136
|
+
Object.entries(modes).filter(([, config]) => config.requiresPrompt)
|
|
137
|
+
);
|
|
138
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Processing Mode Types
|
|
3
|
+
* Generic types for image processing modes across apps
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type ImageProcessingMode =
|
|
7
|
+
| "clean_white"
|
|
8
|
+
| "portrait_blur"
|
|
9
|
+
| "creative_scene"
|
|
10
|
+
| "transparent"
|
|
11
|
+
| "enhance"
|
|
12
|
+
| "remove_object"
|
|
13
|
+
| "replace_object"
|
|
14
|
+
| "relight";
|
|
15
|
+
|
|
16
|
+
export interface ModeConfig {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly icon: string;
|
|
19
|
+
readonly cost: number;
|
|
20
|
+
readonly premium: boolean;
|
|
21
|
+
readonly requiresPrompt: boolean;
|
|
22
|
+
readonly aiPrompt: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ModeCatalog {
|
|
26
|
+
readonly [key: string]: ModeConfig;
|
|
27
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gallery Empty States
|
|
3
|
+
* Handles different empty state scenarios for gallery
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, ActivityIndicator, StyleSheet } from "react-native";
|
|
8
|
+
import type { DesignTokens } from "@umituz/react-native-design-system";
|
|
9
|
+
import { EmptyState } from "./EmptyState";
|
|
10
|
+
import type { Creation } from "../../domain/entities/Creation";
|
|
11
|
+
|
|
12
|
+
interface GalleryEmptyStatesProps {
|
|
13
|
+
isLoading: boolean;
|
|
14
|
+
creations: Creation[] | undefined;
|
|
15
|
+
isFiltered: boolean;
|
|
16
|
+
tokens: DesignTokens;
|
|
17
|
+
t: (key: string) => string;
|
|
18
|
+
emptyTitle: string;
|
|
19
|
+
emptyDescription: string;
|
|
20
|
+
emptyActionLabel?: string;
|
|
21
|
+
onEmptyAction?: () => void;
|
|
22
|
+
onClearFilters: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function GalleryEmptyStates({
|
|
26
|
+
isLoading,
|
|
27
|
+
creations,
|
|
28
|
+
isFiltered,
|
|
29
|
+
tokens,
|
|
30
|
+
t,
|
|
31
|
+
emptyTitle,
|
|
32
|
+
emptyDescription,
|
|
33
|
+
emptyActionLabel,
|
|
34
|
+
onEmptyAction,
|
|
35
|
+
onClearFilters,
|
|
36
|
+
}: GalleryEmptyStatesProps) {
|
|
37
|
+
const styles = createStyles(tokens);
|
|
38
|
+
|
|
39
|
+
// 1. Loading State
|
|
40
|
+
if (isLoading && (!creations || creations?.length === 0)) {
|
|
41
|
+
return (
|
|
42
|
+
<View style={styles.centerContainer}>
|
|
43
|
+
<ActivityIndicator size="large" color={tokens.colors.primary} />
|
|
44
|
+
</View>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 2. System Empty State (User has NO creations at all)
|
|
49
|
+
if (!creations || creations?.length === 0) {
|
|
50
|
+
return (
|
|
51
|
+
<View style={styles.centerContainer}>
|
|
52
|
+
<EmptyState
|
|
53
|
+
title={emptyTitle}
|
|
54
|
+
description={emptyDescription}
|
|
55
|
+
actionLabel={emptyActionLabel}
|
|
56
|
+
onAction={onEmptyAction}
|
|
57
|
+
/>
|
|
58
|
+
</View>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 3. Filter Empty State (User has creations, but filter returns none)
|
|
63
|
+
if (isFiltered) {
|
|
64
|
+
return (
|
|
65
|
+
<View style={styles.centerContainer}>
|
|
66
|
+
<EmptyState
|
|
67
|
+
title={t("common.no_results") || "No results"}
|
|
68
|
+
description={t("common.no_results_description") || "Try changing your filters"}
|
|
69
|
+
actionLabel={t("common.clear_all") || "Clear All"}
|
|
70
|
+
onAction={onClearFilters}
|
|
71
|
+
/>
|
|
72
|
+
</View>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const createStyles = (tokens: DesignTokens) => StyleSheet.create({
|
|
80
|
+
centerContainer: {
|
|
81
|
+
flex: 1,
|
|
82
|
+
justifyContent: 'center',
|
|
83
|
+
alignItems: 'center',
|
|
84
|
+
minHeight: 400,
|
|
85
|
+
paddingHorizontal: tokens.spacing.xl,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
export { GalleryHeader } from "./GalleryHeader";
|
|
6
6
|
export { EmptyState } from "./EmptyState";
|
|
7
|
+
export { GalleryEmptyStates } from "./GalleryEmptyStates";
|
|
7
8
|
export { FilterChips } from "./FilterChips";
|
|
8
9
|
export { CreationsHomeCard } from "./CreationsHomeCard";
|
|
9
10
|
export { CreationCard } from "./CreationCard";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useMemo, useCallback, useState } from "react";
|
|
2
|
-
import { View, StyleSheet,
|
|
2
|
+
import { View, StyleSheet, type LayoutChangeEvent } from "react-native";
|
|
3
3
|
import { useAppDesignTokens, useAlert, AlertType, AlertMode, useSharing, type DesignTokens } from "@umituz/react-native-design-system";
|
|
4
4
|
import { BottomSheetModal } from '@gorhom/bottom-sheet';
|
|
5
5
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
@@ -7,7 +7,7 @@ import { useFocusEffect } from "@react-navigation/native";
|
|
|
7
7
|
import { useCreations } from "../hooks/useCreations";
|
|
8
8
|
import { useDeleteCreation } from "../hooks/useDeleteCreation";
|
|
9
9
|
import { useCreationsFilter } from "../hooks/useCreationsFilter";
|
|
10
|
-
import { GalleryHeader,
|
|
10
|
+
import { GalleryHeader, CreationsGrid, FilterBottomSheet, CreationImageViewer, GalleryEmptyStates } from "../components";
|
|
11
11
|
import { getTranslatedTypes, getFilterCategoriesFromConfig } from "../utils/filterUtils";
|
|
12
12
|
import type { Creation } from "../../domain/entities/Creation";
|
|
13
13
|
import type { CreationsConfig } from "../../domain/value-objects/CreationsConfig";
|
|
@@ -97,43 +97,20 @@ export function CreationsGalleryScreen({
|
|
|
97
97
|
|
|
98
98
|
const styles = useStyles(tokens);
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
<View style={styles.centerContainer}>
|
|
115
|
-
<EmptyState
|
|
116
|
-
title={t(config.translations.empty)}
|
|
117
|
-
description={t(config.translations.emptyDescription)}
|
|
118
|
-
actionLabel={emptyActionLabel}
|
|
119
|
-
onAction={onEmptyAction}
|
|
120
|
-
/>
|
|
121
|
-
</View>
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// 3. Filter Empty State (User has creations, but filter returns none)
|
|
126
|
-
return (
|
|
127
|
-
<View style={styles.centerContainer}>
|
|
128
|
-
<EmptyState
|
|
129
|
-
title={t("common.no_results") || "No results"}
|
|
130
|
-
description={t("common.no_results_description") || "Try changing your filters"}
|
|
131
|
-
actionLabel={t("common.clear_all") || "Clear All"}
|
|
132
|
-
onAction={clearFilters}
|
|
133
|
-
/>
|
|
134
|
-
</View>
|
|
135
|
-
);
|
|
136
|
-
}, [isLoading, creations, config, t, emptyActionLabel, onEmptyAction, clearFilters, styles.centerContainer, tokens.colors.primary]);
|
|
100
|
+
const renderEmptyComponent = useMemo(() => (
|
|
101
|
+
<GalleryEmptyStates
|
|
102
|
+
isLoading={isLoading}
|
|
103
|
+
creations={creations}
|
|
104
|
+
isFiltered={isFiltered}
|
|
105
|
+
tokens={tokens}
|
|
106
|
+
t={t}
|
|
107
|
+
emptyTitle={t(config.translations.empty)}
|
|
108
|
+
emptyDescription={t(config.translations.emptyDescription)}
|
|
109
|
+
emptyActionLabel={emptyActionLabel}
|
|
110
|
+
onEmptyAction={onEmptyAction}
|
|
111
|
+
onClearFilters={clearFilters}
|
|
112
|
+
/>
|
|
113
|
+
), [isLoading, creations, isFiltered, tokens, t, config, emptyActionLabel, onEmptyAction, clearFilters]);
|
|
137
114
|
|
|
138
115
|
if (selectedCreation) {
|
|
139
116
|
return (
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Service Processor
|
|
3
|
+
* Handles processing of different AI service types
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { FaceSwapConfig } from '../../domain/entities/FaceSwapConfig';
|
|
7
|
+
import type { PhotoRestorationConfig } from '../../domain/entities/PhotoRestorationConfig';
|
|
8
|
+
import type { ImageEnhancementConfig } from '../../domain/entities/ImageEnhancementConfig';
|
|
9
|
+
import type { StyleTransferConfig } from '../../domain/entities/StyleTransferConfig';
|
|
10
|
+
import type { BackgroundRemovalConfig } from '../../domain/entities/BackgroundRemovalConfig';
|
|
11
|
+
import type { TextGenerationConfig } from '../../domain/entities/TextGenerationConfig';
|
|
12
|
+
import type { ColorizationConfig } from '../../domain/entities/ColorizationConfig';
|
|
13
|
+
import type {
|
|
14
|
+
IFaceSwapService,
|
|
15
|
+
IPhotoRestorationService,
|
|
16
|
+
IImageEnhancementService,
|
|
17
|
+
IStyleTransferService,
|
|
18
|
+
IBackgroundRemovalService,
|
|
19
|
+
ITextGenerationService,
|
|
20
|
+
IColorizationService,
|
|
21
|
+
} from '../../domain/repositories/IAIPromptServices';
|
|
22
|
+
import type { AIPromptResult } from '../../domain/entities/types';
|
|
23
|
+
import type { AIPromptTemplate } from '../../domain/entities/AIPromptTemplate';
|
|
24
|
+
|
|
25
|
+
export type AIConfig =
|
|
26
|
+
| { type: 'face-swap'; config: FaceSwapConfig }
|
|
27
|
+
| { type: 'photo-restoration'; config: PhotoRestorationConfig }
|
|
28
|
+
| { type: 'image-enhancement'; config: ImageEnhancementConfig }
|
|
29
|
+
| { type: 'style-transfer'; config: StyleTransferConfig }
|
|
30
|
+
| { type: 'background-removal'; config: BackgroundRemovalConfig }
|
|
31
|
+
| { type: 'text-generation'; config: TextGenerationConfig }
|
|
32
|
+
| { type: 'colorization'; config: ColorizationConfig };
|
|
33
|
+
|
|
34
|
+
export interface AIServices {
|
|
35
|
+
faceSwap: IFaceSwapService;
|
|
36
|
+
photoRestoration: IPhotoRestorationService;
|
|
37
|
+
imageEnhancement: IImageEnhancementService;
|
|
38
|
+
styleTransfer: IStyleTransferService;
|
|
39
|
+
backgroundRemoval: IBackgroundRemovalService;
|
|
40
|
+
textGeneration: ITextGenerationService;
|
|
41
|
+
colorization: IColorizationService;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ProcessResult {
|
|
45
|
+
template: AIPromptTemplate;
|
|
46
|
+
prompt: string;
|
|
47
|
+
config: Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class AIServiceProcessor {
|
|
51
|
+
constructor(private services: AIServices) { }
|
|
52
|
+
|
|
53
|
+
async process(aiConfig: AIConfig): Promise<ProcessResult> {
|
|
54
|
+
const { templateResult, promptResult } = await this.executeService(aiConfig);
|
|
55
|
+
|
|
56
|
+
if (!templateResult?.success || !templateResult.data) {
|
|
57
|
+
throw new Error('Failed to generate template');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!promptResult?.success || !promptResult.data) {
|
|
61
|
+
throw new Error('Failed to generate prompt');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
template: templateResult.data,
|
|
66
|
+
prompt: promptResult.data,
|
|
67
|
+
config: aiConfig.config as unknown as Record<string, unknown>,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async getAvailableStyles(serviceType: string): Promise<string[]> {
|
|
72
|
+
switch (serviceType) {
|
|
73
|
+
case 'face-swap':
|
|
74
|
+
return await this.services.faceSwap.getAvailableStyles();
|
|
75
|
+
case 'style-transfer':
|
|
76
|
+
return await this.services.styleTransfer.getAvailableStyles();
|
|
77
|
+
default:
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private async executeService(aiConfig: AIConfig): Promise<{
|
|
83
|
+
templateResult: AIPromptResult<AIPromptTemplate> | undefined;
|
|
84
|
+
promptResult: AIPromptResult<string> | undefined;
|
|
85
|
+
}> {
|
|
86
|
+
let templateResult: AIPromptResult<AIPromptTemplate> | undefined;
|
|
87
|
+
let promptResult: AIPromptResult<string> | undefined;
|
|
88
|
+
|
|
89
|
+
switch (aiConfig.type) {
|
|
90
|
+
case 'face-swap':
|
|
91
|
+
templateResult = await this.services.faceSwap.generateTemplate(aiConfig.config);
|
|
92
|
+
if (templateResult.success && templateResult.data) {
|
|
93
|
+
promptResult = await this.services.faceSwap.generatePrompt(templateResult.data, aiConfig.config);
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
|
|
97
|
+
case 'photo-restoration':
|
|
98
|
+
templateResult = await this.services.photoRestoration.generateTemplate(aiConfig.config);
|
|
99
|
+
if (templateResult.success && templateResult.data) {
|
|
100
|
+
promptResult = await this.services.photoRestoration.generatePrompt(templateResult.data, aiConfig.config);
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
|
|
104
|
+
case 'image-enhancement':
|
|
105
|
+
templateResult = await this.services.imageEnhancement.generateTemplate(aiConfig.config);
|
|
106
|
+
if (templateResult.success && templateResult.data) {
|
|
107
|
+
promptResult = await this.services.imageEnhancement.generatePrompt(templateResult.data, aiConfig.config);
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
|
|
111
|
+
case 'style-transfer':
|
|
112
|
+
templateResult = await this.services.styleTransfer.generateTemplate(aiConfig.config);
|
|
113
|
+
if (templateResult.success && templateResult.data) {
|
|
114
|
+
promptResult = await this.services.styleTransfer.generatePrompt(templateResult.data, aiConfig.config);
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
|
|
118
|
+
case 'background-removal':
|
|
119
|
+
templateResult = await this.services.backgroundRemoval.generateTemplate(aiConfig.config);
|
|
120
|
+
if (templateResult.success && templateResult.data) {
|
|
121
|
+
promptResult = await this.services.backgroundRemoval.generatePrompt(templateResult.data, aiConfig.config);
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
|
|
125
|
+
case 'text-generation':
|
|
126
|
+
templateResult = await this.services.textGeneration.generateTemplate(aiConfig.config);
|
|
127
|
+
if (templateResult.success && templateResult.data) {
|
|
128
|
+
promptResult = await this.services.textGeneration.generatePrompt(templateResult.data, aiConfig.config);
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
|
|
132
|
+
case 'colorization':
|
|
133
|
+
templateResult = await this.services.colorization.generateTemplate(aiConfig.config);
|
|
134
|
+
if (templateResult.success && templateResult.data) {
|
|
135
|
+
promptResult = await this.services.colorization.generatePrompt(templateResult.data, aiConfig.config);
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { templateResult, promptResult };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -1,50 +1,12 @@
|
|
|
1
|
-
import { useState, useCallback } from 'react';
|
|
2
|
-
import type {
|
|
3
|
-
FaceSwapConfig
|
|
4
|
-
} from '../../domain/entities/FaceSwapConfig';
|
|
5
|
-
import type {
|
|
6
|
-
PhotoRestorationConfig
|
|
7
|
-
} from '../../domain/entities/PhotoRestorationConfig';
|
|
8
|
-
import type {
|
|
9
|
-
ImageEnhancementConfig
|
|
10
|
-
} from '../../domain/entities/ImageEnhancementConfig';
|
|
11
|
-
import type {
|
|
12
|
-
StyleTransferConfig
|
|
13
|
-
} from '../../domain/entities/StyleTransferConfig';
|
|
14
|
-
import type {
|
|
15
|
-
BackgroundRemovalConfig
|
|
16
|
-
} from '../../domain/entities/BackgroundRemovalConfig';
|
|
17
|
-
import type {
|
|
18
|
-
TextGenerationConfig
|
|
19
|
-
} from '../../domain/entities/TextGenerationConfig';
|
|
20
|
-
import type {
|
|
21
|
-
ColorizationConfig
|
|
22
|
-
} from '../../domain/entities/ColorizationConfig';
|
|
23
|
-
import type {
|
|
24
|
-
IFaceSwapService,
|
|
25
|
-
IPhotoRestorationService,
|
|
26
|
-
IImageEnhancementService,
|
|
27
|
-
IStyleTransferService,
|
|
28
|
-
IBackgroundRemovalService,
|
|
29
|
-
ITextGenerationService,
|
|
30
|
-
IColorizationService
|
|
31
|
-
} from '../../domain/repositories/IAIPromptServices';
|
|
1
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
32
2
|
import type { ITemplateRepository } from '../../domain/repositories/ITemplateRepository';
|
|
33
3
|
import type { IPromptHistoryRepository } from '../../domain/repositories/IPromptHistoryRepository';
|
|
34
4
|
import type { GeneratedPrompt } from '../../domain/entities/GeneratedPrompt';
|
|
35
5
|
import { createGeneratedPrompt } from '../../domain/entities/GeneratedPrompt';
|
|
36
6
|
import { useAsyncState } from './useAsyncState';
|
|
37
|
-
import type
|
|
38
|
-
import type { AIPromptTemplate } from '../../domain/entities/AIPromptTemplate';
|
|
7
|
+
import { AIServiceProcessor, type AIConfig, type AIServices } from '../../infrastructure/services/AIServiceProcessor';
|
|
39
8
|
|
|
40
|
-
export type AIConfig
|
|
41
|
-
| { type: 'face-swap'; config: FaceSwapConfig }
|
|
42
|
-
| { type: 'photo-restoration'; config: PhotoRestorationConfig }
|
|
43
|
-
| { type: 'image-enhancement'; config: ImageEnhancementConfig }
|
|
44
|
-
| { type: 'style-transfer'; config: StyleTransferConfig }
|
|
45
|
-
| { type: 'background-removal'; config: BackgroundRemovalConfig }
|
|
46
|
-
| { type: 'text-generation'; config: TextGenerationConfig }
|
|
47
|
-
| { type: 'colorization'; config: ColorizationConfig };
|
|
9
|
+
export type { AIConfig };
|
|
48
10
|
|
|
49
11
|
export interface UseAIServicesState {
|
|
50
12
|
generatedPrompt: GeneratedPrompt | null;
|
|
@@ -60,15 +22,7 @@ export interface UseAIServicesActions {
|
|
|
60
22
|
}
|
|
61
23
|
|
|
62
24
|
export const useAIServices = (
|
|
63
|
-
services:
|
|
64
|
-
faceSwap: IFaceSwapService;
|
|
65
|
-
photoRestoration: IPhotoRestorationService;
|
|
66
|
-
imageEnhancement: IImageEnhancementService;
|
|
67
|
-
styleTransfer: IStyleTransferService;
|
|
68
|
-
backgroundRemoval: IBackgroundRemovalService;
|
|
69
|
-
textGeneration: ITextGenerationService;
|
|
70
|
-
colorization: IColorizationService;
|
|
71
|
-
},
|
|
25
|
+
services: AIServices,
|
|
72
26
|
repositories: {
|
|
73
27
|
template: ITemplateRepository;
|
|
74
28
|
history: IPromptHistoryRepository;
|
|
@@ -85,110 +39,39 @@ export const useAIServices = (
|
|
|
85
39
|
|
|
86
40
|
const [currentService, setCurrentService] = useState<string | null>(null);
|
|
87
41
|
|
|
42
|
+
const processor = useMemo(() => new AIServiceProcessor(services), [services]);
|
|
43
|
+
|
|
88
44
|
const processRequest = useCallback(async (aiConfig: AIConfig): Promise<void> => {
|
|
89
45
|
clearError();
|
|
90
46
|
setCurrentService(aiConfig.type);
|
|
91
47
|
|
|
92
48
|
try {
|
|
93
|
-
|
|
94
|
-
let promptResult: AIPromptResult<string> | undefined;
|
|
95
|
-
|
|
96
|
-
switch (aiConfig.type) {
|
|
97
|
-
case 'face-swap':
|
|
98
|
-
templateResult = await services.faceSwap.generateTemplate(aiConfig.config);
|
|
99
|
-
if (templateResult.success && templateResult.data) {
|
|
100
|
-
promptResult = await services.faceSwap.generatePrompt(templateResult.data, aiConfig.config);
|
|
101
|
-
}
|
|
102
|
-
break;
|
|
103
|
-
|
|
104
|
-
case 'photo-restoration':
|
|
105
|
-
templateResult = await services.photoRestoration.generateTemplate(aiConfig.config);
|
|
106
|
-
if (templateResult.success && templateResult.data) {
|
|
107
|
-
promptResult = await services.photoRestoration.generatePrompt(templateResult.data, aiConfig.config);
|
|
108
|
-
}
|
|
109
|
-
break;
|
|
110
|
-
|
|
111
|
-
case 'image-enhancement':
|
|
112
|
-
templateResult = await services.imageEnhancement.generateTemplate(aiConfig.config);
|
|
113
|
-
if (templateResult.success && templateResult.data) {
|
|
114
|
-
promptResult = await services.imageEnhancement.generatePrompt(templateResult.data, aiConfig.config);
|
|
115
|
-
}
|
|
116
|
-
break;
|
|
117
|
-
|
|
118
|
-
case 'style-transfer':
|
|
119
|
-
templateResult = await services.styleTransfer.generateTemplate(aiConfig.config);
|
|
120
|
-
if (templateResult.success && templateResult.data) {
|
|
121
|
-
promptResult = await services.styleTransfer.generatePrompt(templateResult.data, aiConfig.config);
|
|
122
|
-
}
|
|
123
|
-
break;
|
|
124
|
-
|
|
125
|
-
case 'background-removal':
|
|
126
|
-
templateResult = await services.backgroundRemoval.generateTemplate(aiConfig.config);
|
|
127
|
-
if (templateResult.success && templateResult.data) {
|
|
128
|
-
promptResult = await services.backgroundRemoval.generatePrompt(templateResult.data, aiConfig.config);
|
|
129
|
-
}
|
|
130
|
-
break;
|
|
131
|
-
|
|
132
|
-
case 'text-generation':
|
|
133
|
-
templateResult = await services.textGeneration.generateTemplate(aiConfig.config);
|
|
134
|
-
if (templateResult.success && templateResult.data) {
|
|
135
|
-
promptResult = await services.textGeneration.generatePrompt(templateResult.data, aiConfig.config);
|
|
136
|
-
}
|
|
137
|
-
break;
|
|
138
|
-
|
|
139
|
-
case 'colorization':
|
|
140
|
-
templateResult = await services.colorization.generateTemplate(aiConfig.config);
|
|
141
|
-
if (templateResult.success && templateResult.data) {
|
|
142
|
-
promptResult = await services.colorization.generatePrompt(templateResult.data, aiConfig.config);
|
|
143
|
-
}
|
|
144
|
-
break;
|
|
145
|
-
|
|
146
|
-
default:
|
|
147
|
-
setError('Unknown AI service type');
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (!templateResult?.success || !templateResult.data) {
|
|
152
|
-
setError('Failed to generate template');
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (!promptResult?.success || !promptResult.data) {
|
|
157
|
-
setError('Failed to generate prompt');
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
49
|
+
const result = await processor.process(aiConfig);
|
|
160
50
|
|
|
161
51
|
const newPrompt = createGeneratedPrompt({
|
|
162
|
-
templateId:
|
|
163
|
-
generatedText:
|
|
164
|
-
variables:
|
|
52
|
+
templateId: result.template.id,
|
|
53
|
+
generatedText: result.prompt,
|
|
54
|
+
variables: result.config,
|
|
165
55
|
});
|
|
166
56
|
|
|
167
57
|
await repositories.history.save(newPrompt);
|
|
168
58
|
setGeneratedPrompt(newPrompt);
|
|
169
59
|
|
|
170
|
-
} catch {
|
|
171
|
-
setError('An unexpected error occurred');
|
|
60
|
+
} catch (err) {
|
|
61
|
+
setError(err instanceof Error ? err.message : 'An unexpected error occurred');
|
|
172
62
|
} finally {
|
|
173
63
|
setCurrentService(null);
|
|
174
64
|
}
|
|
175
|
-
}, [
|
|
65
|
+
}, [processor, repositories, setError, setGeneratedPrompt, clearError]);
|
|
176
66
|
|
|
177
67
|
const getAvailableStyles = useCallback(async (serviceType: string): Promise<string[]> => {
|
|
178
68
|
try {
|
|
179
|
-
|
|
180
|
-
case 'face-swap':
|
|
181
|
-
return await services.faceSwap.getAvailableStyles();
|
|
182
|
-
case 'style-transfer':
|
|
183
|
-
return await services.styleTransfer.getAvailableStyles();
|
|
184
|
-
default:
|
|
185
|
-
return [];
|
|
186
|
-
}
|
|
69
|
+
return await processor.getAvailableStyles(serviceType);
|
|
187
70
|
} catch {
|
|
188
71
|
setError('Failed to load available styles');
|
|
189
72
|
return [];
|
|
190
73
|
}
|
|
191
|
-
}, [
|
|
74
|
+
}, [processor, setError]);
|
|
192
75
|
|
|
193
76
|
const clearPrompt = useCallback(() => {
|
|
194
77
|
setGeneratedPrompt(null);
|
package/src/index.ts
CHANGED
|
@@ -60,6 +60,24 @@ export type {
|
|
|
60
60
|
|
|
61
61
|
export { DEFAULT_POLLING_CONFIG, DEFAULT_PROGRESS_STAGES, DEFAULT_QUEUE_CONFIG } from "./domain/entities";
|
|
62
62
|
|
|
63
|
+
// =============================================================================
|
|
64
|
+
// DOMAIN LAYER - Processing Modes
|
|
65
|
+
// =============================================================================
|
|
66
|
+
|
|
67
|
+
export type {
|
|
68
|
+
ImageProcessingMode,
|
|
69
|
+
ModeConfig,
|
|
70
|
+
ModeCatalog,
|
|
71
|
+
} from "./domain/entities/processing-modes.types";
|
|
72
|
+
|
|
73
|
+
export {
|
|
74
|
+
DEFAULT_PROCESSING_MODES,
|
|
75
|
+
getModeConfig,
|
|
76
|
+
getFreeModes,
|
|
77
|
+
getPremiumModes,
|
|
78
|
+
getPromptRequiredModes,
|
|
79
|
+
} from "./domain/constants/processing-modes.constants";
|
|
80
|
+
|
|
63
81
|
// =============================================================================
|
|
64
82
|
// INFRASTRUCTURE LAYER - Services
|
|
65
83
|
// =============================================================================
|
|
@@ -9,15 +9,10 @@ import type {
|
|
|
9
9
|
GenerationProgress,
|
|
10
10
|
PollingConfig,
|
|
11
11
|
} from "../../domain/entities";
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
classifyError,
|
|
17
|
-
isTransientError,
|
|
18
|
-
} from "../utils/error-classifier.util";
|
|
19
|
-
import { createPollingDelay } from "../utils/polling-interval.util";
|
|
20
|
-
import { createProgressTracker } from "../utils/progress-calculator.util";
|
|
12
|
+
import { classifyError } from "../utils/error-classifier.util";
|
|
13
|
+
import { ProgressManager } from "./progress-manager";
|
|
14
|
+
import { JobPoller, type PollerConfig } from "./job-poller";
|
|
15
|
+
import { ProviderValidator } from "./provider-validator";
|
|
21
16
|
|
|
22
17
|
declare const __DEV__: boolean;
|
|
23
18
|
|
|
@@ -27,7 +22,9 @@ export interface OrchestratorConfig {
|
|
|
27
22
|
}
|
|
28
23
|
|
|
29
24
|
class GenerationOrchestratorService {
|
|
30
|
-
private
|
|
25
|
+
private progressManager = new ProgressManager();
|
|
26
|
+
private jobPoller = new JobPoller();
|
|
27
|
+
private providerValidator = new ProviderValidator();
|
|
31
28
|
|
|
32
29
|
configure(config: OrchestratorConfig): void {
|
|
33
30
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
@@ -37,14 +34,19 @@ class GenerationOrchestratorService {
|
|
|
37
34
|
hasStatusUpdate: !!config.onStatusUpdate,
|
|
38
35
|
});
|
|
39
36
|
}
|
|
40
|
-
|
|
37
|
+
|
|
38
|
+
const pollerConfig: PollerConfig = {
|
|
39
|
+
polling: config.polling,
|
|
40
|
+
onStatusUpdate: config.onStatusUpdate,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
this.jobPoller.configure(pollerConfig);
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
async generate<T = unknown>(
|
|
44
47
|
request: GenerationRequest,
|
|
45
48
|
): Promise<GenerationResult<T>> {
|
|
46
|
-
const provider = this.getProvider();
|
|
47
|
-
const progressTracker = createProgressTracker();
|
|
49
|
+
const provider = this.providerValidator.getProvider();
|
|
48
50
|
const startTime = Date.now();
|
|
49
51
|
|
|
50
52
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
@@ -60,11 +62,7 @@ class GenerationOrchestratorService {
|
|
|
60
62
|
stage: GenerationProgress["stage"],
|
|
61
63
|
subProgress = 0,
|
|
62
64
|
) => {
|
|
63
|
-
|
|
64
|
-
request.onProgress?.({
|
|
65
|
-
stage,
|
|
66
|
-
progress: progress + subProgress,
|
|
67
|
-
});
|
|
65
|
+
this.progressManager.updateProgress(stage, subProgress, request.onProgress);
|
|
68
66
|
};
|
|
69
67
|
|
|
70
68
|
try {
|
|
@@ -84,11 +82,19 @@ class GenerationOrchestratorService {
|
|
|
84
82
|
|
|
85
83
|
updateProgress("generating");
|
|
86
84
|
|
|
87
|
-
const result = await this.pollForResult<T>(
|
|
85
|
+
const result = await this.jobPoller.pollForResult<T>(
|
|
88
86
|
provider,
|
|
89
87
|
request.model,
|
|
90
88
|
submission.requestId,
|
|
91
89
|
request.onProgress,
|
|
90
|
+
(status, attempt, config) => {
|
|
91
|
+
this.progressManager.updateProgressFromStatus(
|
|
92
|
+
status,
|
|
93
|
+
attempt,
|
|
94
|
+
config,
|
|
95
|
+
request.onProgress,
|
|
96
|
+
);
|
|
97
|
+
},
|
|
92
98
|
);
|
|
93
99
|
|
|
94
100
|
updateProgress("completed");
|
|
@@ -139,149 +145,6 @@ class GenerationOrchestratorService {
|
|
|
139
145
|
};
|
|
140
146
|
}
|
|
141
147
|
}
|
|
142
|
-
|
|
143
|
-
private async pollForResult<T>(
|
|
144
|
-
provider: IAIProvider,
|
|
145
|
-
model: string,
|
|
146
|
-
requestId: string,
|
|
147
|
-
onProgress?: (progress: GenerationProgress) => void,
|
|
148
|
-
): Promise<T> {
|
|
149
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
150
|
-
// eslint-disable-next-line no-console
|
|
151
|
-
console.log("[Orchestrator] pollForResult() started", {
|
|
152
|
-
provider: provider.providerId,
|
|
153
|
-
model,
|
|
154
|
-
requestId,
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const config = {
|
|
159
|
-
...DEFAULT_POLLING_CONFIG,
|
|
160
|
-
...this.config.polling,
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
let consecutiveErrors = 0;
|
|
164
|
-
|
|
165
|
-
for (let attempt = 0; attempt < config.maxAttempts; attempt++) {
|
|
166
|
-
await createPollingDelay(attempt, config);
|
|
167
|
-
|
|
168
|
-
if (typeof __DEV__ !== "undefined" && __DEV__ && attempt % 5 === 0) {
|
|
169
|
-
// eslint-disable-next-line no-console
|
|
170
|
-
console.log("[Orchestrator] pollForResult() attempt", {
|
|
171
|
-
attempt,
|
|
172
|
-
maxAttempts: config.maxAttempts,
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
try {
|
|
177
|
-
const status = await provider.getJobStatus(model, requestId);
|
|
178
|
-
|
|
179
|
-
consecutiveErrors = 0;
|
|
180
|
-
|
|
181
|
-
this.updateProgressFromStatus(status, attempt, config, onProgress);
|
|
182
|
-
|
|
183
|
-
if (status.status === "COMPLETED") {
|
|
184
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
185
|
-
// eslint-disable-next-line no-console
|
|
186
|
-
console.log("[Orchestrator] pollForResult() job COMPLETED", {
|
|
187
|
-
requestId,
|
|
188
|
-
attempt,
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
return provider.getJobResult<T>(model, requestId);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (status.status === "FAILED") {
|
|
195
|
-
throw new Error("Job failed on provider");
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
await this.config.onStatusUpdate?.(requestId, status.status);
|
|
199
|
-
} catch (error) {
|
|
200
|
-
if (isTransientError(error)) {
|
|
201
|
-
consecutiveErrors++;
|
|
202
|
-
|
|
203
|
-
if (consecutiveErrors >= config.maxConsecutiveErrors) {
|
|
204
|
-
throw error;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
throw error;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
throw new Error(
|
|
215
|
-
`Polling timeout after ${config.maxAttempts} attempts`,
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
private updateProgressFromStatus(
|
|
220
|
-
status: JobStatus,
|
|
221
|
-
attempt: number,
|
|
222
|
-
config: PollingConfig,
|
|
223
|
-
onProgress?: (progress: GenerationProgress) => void,
|
|
224
|
-
): void {
|
|
225
|
-
const baseProgress = 25;
|
|
226
|
-
const maxProgress = 85;
|
|
227
|
-
const range = maxProgress - baseProgress;
|
|
228
|
-
|
|
229
|
-
let progress: number;
|
|
230
|
-
|
|
231
|
-
if (status.status === "IN_QUEUE") {
|
|
232
|
-
progress = baseProgress + range * 0.2;
|
|
233
|
-
} else if (status.status === "IN_PROGRESS") {
|
|
234
|
-
const ratio = Math.min(attempt / (config.maxAttempts * 0.7), 1);
|
|
235
|
-
progress = baseProgress + range * (0.2 + 0.6 * ratio);
|
|
236
|
-
} else {
|
|
237
|
-
progress = baseProgress;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
onProgress?.({
|
|
241
|
-
stage: "generating",
|
|
242
|
-
progress: Math.round(progress),
|
|
243
|
-
eta: status.eta,
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
private getProvider(): IAIProvider {
|
|
248
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
249
|
-
// eslint-disable-next-line no-console
|
|
250
|
-
console.log("[Orchestrator] getProvider() called");
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const provider = providerRegistry.getActiveProvider();
|
|
254
|
-
|
|
255
|
-
if (!provider) {
|
|
256
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
257
|
-
// eslint-disable-next-line no-console
|
|
258
|
-
console.error("[Orchestrator] No active provider found!");
|
|
259
|
-
}
|
|
260
|
-
throw new Error(
|
|
261
|
-
"No active AI provider. Register and set a provider first.",
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (!provider.isInitialized()) {
|
|
266
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
267
|
-
// eslint-disable-next-line no-console
|
|
268
|
-
console.error("[Orchestrator] Provider not initialized:", provider.providerId);
|
|
269
|
-
}
|
|
270
|
-
throw new Error(
|
|
271
|
-
`Provider ${provider.providerId} is not initialized.`,
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
276
|
-
// eslint-disable-next-line no-console
|
|
277
|
-
console.log("[Orchestrator] getProvider() returning:", {
|
|
278
|
-
providerId: provider.providerId,
|
|
279
|
-
isInitialized: provider.isInitialized(),
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return provider;
|
|
284
|
-
}
|
|
285
148
|
}
|
|
286
149
|
|
|
287
150
|
export const generationOrchestrator = new GenerationOrchestratorService();
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job Poller
|
|
3
|
+
* Handles polling logic for job status
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { IAIProvider, JobStatus } from "../../domain/interfaces";
|
|
7
|
+
import { DEFAULT_POLLING_CONFIG, type PollingConfig, type GenerationProgress } from "../../domain/entities";
|
|
8
|
+
import { isTransientError } from "../utils/error-classifier.util";
|
|
9
|
+
import { createPollingDelay } from "../utils/polling-interval.util";
|
|
10
|
+
|
|
11
|
+
declare const __DEV__: boolean;
|
|
12
|
+
|
|
13
|
+
export interface PollerConfig {
|
|
14
|
+
polling?: Partial<PollingConfig>;
|
|
15
|
+
onStatusUpdate?: (requestId: string, status: string) => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class JobPoller {
|
|
19
|
+
private config: PollerConfig = {};
|
|
20
|
+
|
|
21
|
+
configure(config: PollerConfig): void {
|
|
22
|
+
this.config = { ...this.config, ...config };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async pollForResult<T>(
|
|
26
|
+
provider: IAIProvider,
|
|
27
|
+
model: string,
|
|
28
|
+
requestId: string,
|
|
29
|
+
onProgress?: (progress: GenerationProgress) => void,
|
|
30
|
+
onStatusUpdate?: (status: JobStatus, attempt: number, config: PollingConfig) => void,
|
|
31
|
+
): Promise<T> {
|
|
32
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
33
|
+
// eslint-disable-next-line no-console
|
|
34
|
+
console.log("[JobPoller] pollForResult() started", {
|
|
35
|
+
provider: provider.providerId,
|
|
36
|
+
model,
|
|
37
|
+
requestId,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const config = {
|
|
42
|
+
...DEFAULT_POLLING_CONFIG,
|
|
43
|
+
...this.config.polling,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
let consecutiveErrors = 0;
|
|
47
|
+
|
|
48
|
+
for (let attempt = 0; attempt < config.maxAttempts; attempt++) {
|
|
49
|
+
await createPollingDelay(attempt, config);
|
|
50
|
+
|
|
51
|
+
if (typeof __DEV__ !== "undefined" && __DEV__ && attempt % 5 === 0) {
|
|
52
|
+
// eslint-disable-next-line no-console
|
|
53
|
+
console.log("[JobPoller] pollForResult() attempt", {
|
|
54
|
+
attempt,
|
|
55
|
+
maxAttempts: config.maxAttempts,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const status = await provider.getJobStatus(model, requestId);
|
|
61
|
+
|
|
62
|
+
consecutiveErrors = 0;
|
|
63
|
+
|
|
64
|
+
onStatusUpdate?.(status, attempt, config);
|
|
65
|
+
|
|
66
|
+
if (status.status === "COMPLETED") {
|
|
67
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
68
|
+
// eslint-disable-next-line no-console
|
|
69
|
+
console.log("[JobPoller] pollForResult() job COMPLETED", {
|
|
70
|
+
requestId,
|
|
71
|
+
attempt,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return provider.getJobResult<T>(model, requestId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (status.status === "FAILED") {
|
|
78
|
+
throw new Error("Job failed on provider");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await this.config.onStatusUpdate?.(requestId, status.status);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (isTransientError(error)) {
|
|
84
|
+
consecutiveErrors++;
|
|
85
|
+
|
|
86
|
+
if (consecutiveErrors >= config.maxConsecutiveErrors) {
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Polling timeout after ${config.maxAttempts} attempts`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const jobPoller = new JobPoller();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress Manager
|
|
3
|
+
* Handles progress tracking and updates during generation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { GenerationProgress, PollingConfig } from "../../domain/entities";
|
|
7
|
+
import type { JobStatus } from "../../domain/interfaces";
|
|
8
|
+
import { createProgressTracker } from "../utils/progress-calculator.util";
|
|
9
|
+
|
|
10
|
+
export class ProgressManager {
|
|
11
|
+
private progressTracker = createProgressTracker();
|
|
12
|
+
|
|
13
|
+
updateProgress(
|
|
14
|
+
stage: GenerationProgress["stage"],
|
|
15
|
+
subProgress: number,
|
|
16
|
+
onProgress?: (progress: GenerationProgress) => void,
|
|
17
|
+
): void {
|
|
18
|
+
const progress = this.progressTracker.setStatus(stage);
|
|
19
|
+
onProgress?.({
|
|
20
|
+
stage,
|
|
21
|
+
progress: progress + subProgress,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
updateProgressFromStatus(
|
|
26
|
+
status: JobStatus,
|
|
27
|
+
attempt: number,
|
|
28
|
+
config: PollingConfig,
|
|
29
|
+
onProgress?: (progress: GenerationProgress) => void,
|
|
30
|
+
): void {
|
|
31
|
+
const baseProgress = 25;
|
|
32
|
+
const maxProgress = 85;
|
|
33
|
+
const range = maxProgress - baseProgress;
|
|
34
|
+
|
|
35
|
+
let progress: number;
|
|
36
|
+
|
|
37
|
+
if (status.status === "IN_QUEUE") {
|
|
38
|
+
progress = baseProgress + range * 0.2;
|
|
39
|
+
} else if (status.status === "IN_PROGRESS") {
|
|
40
|
+
const ratio = Math.min(attempt / (config.maxAttempts * 0.7), 1);
|
|
41
|
+
progress = baseProgress + range * (0.2 + 0.6 * ratio);
|
|
42
|
+
} else {
|
|
43
|
+
progress = baseProgress;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
onProgress?.({
|
|
47
|
+
stage: "generating",
|
|
48
|
+
progress: Math.round(progress),
|
|
49
|
+
eta: status.eta,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
reset(): void {
|
|
54
|
+
this.progressTracker = createProgressTracker();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const progressManager = new ProgressManager();
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Validator
|
|
3
|
+
* Validates provider availability and initialization
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { IAIProvider } from "../../domain/interfaces";
|
|
7
|
+
import { providerRegistry } from "./provider-registry.service";
|
|
8
|
+
|
|
9
|
+
declare const __DEV__: boolean;
|
|
10
|
+
|
|
11
|
+
export class ProviderValidator {
|
|
12
|
+
getProvider(): IAIProvider {
|
|
13
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
14
|
+
// eslint-disable-next-line no-console
|
|
15
|
+
console.log("[ProviderValidator] getProvider() called");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const provider = providerRegistry.getActiveProvider();
|
|
19
|
+
|
|
20
|
+
if (!provider) {
|
|
21
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
22
|
+
// eslint-disable-next-line no-console
|
|
23
|
+
console.error("[ProviderValidator] No active provider found!");
|
|
24
|
+
}
|
|
25
|
+
throw new Error(
|
|
26
|
+
"No active AI provider. Register and set a provider first.",
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!provider.isInitialized()) {
|
|
31
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
32
|
+
// eslint-disable-next-line no-console
|
|
33
|
+
console.error("[ProviderValidator] Provider not initialized:", provider.providerId);
|
|
34
|
+
}
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Provider ${provider.providerId} is not initialized.`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
41
|
+
// eslint-disable-next-line no-console
|
|
42
|
+
console.log("[ProviderValidator] getProvider() returning:", {
|
|
43
|
+
providerId: provider.providerId,
|
|
44
|
+
isInitialized: provider.isInitialized(),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return provider;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const providerValidator = new ProviderValidator();
|