@umituz/react-native-ai-pruna-provider 1.0.65 → 1.0.66

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-pruna-provider",
3
- "version": "1.0.65",
3
+ "version": "1.0.66",
4
4
  "description": "Pruna AI provider for React Native - implements IAIProvider interface for unified AI generation",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -66,16 +66,6 @@ export class PrunaService {
66
66
  const apiKey = this.ensureInitialized();
67
67
  return generateImageEditUseCase.execute(input, apiKey.toString(), signal);
68
68
  }
69
-
70
- reset(): void {
71
- this.apiKey = null;
72
- this.initialized = false;
73
- }
74
-
75
- getSessionLogs(sessionId: string): unknown[] {
76
- // Return logs for debugging
77
- return [];
78
- }
79
69
  }
80
70
 
81
71
  export const prunaService = new PrunaService();
@@ -16,6 +16,7 @@ export enum PrunaErrorType {
16
16
  FILE_UPLOAD = "file_upload",
17
17
  POLLING_TIMEOUT = "polling_timeout",
18
18
  INVALID_IMAGE = "invalid_image",
19
+ CANCELLED = "cancelled",
19
20
  UNKNOWN = "unknown",
20
21
  }
21
22
 
@@ -178,18 +178,15 @@ export class ErrorMapperService {
178
178
  stack?: string,
179
179
  statusCode?: number
180
180
  ): PrunaErrorInfo {
181
- const error: PrunaErrorInfo = {
181
+ return {
182
182
  type,
183
183
  messageKey,
184
184
  retryable,
185
185
  originalError,
186
+ ...(originalErrorName && { originalErrorName }),
187
+ ...(stack && { stack }),
188
+ ...(statusCode !== undefined && { statusCode }),
186
189
  };
187
-
188
- if (originalErrorName) error.originalErrorName = originalErrorName;
189
- if (stack) error.stack = stack;
190
- if (statusCode !== undefined) error.statusCode = statusCode;
191
-
192
- return error;
193
190
  }
194
191
 
195
192
  private static extractMessage(error: unknown): string {
@@ -22,9 +22,4 @@ export class ApiKey {
22
22
  toString(): string {
23
23
  return this.value;
24
24
  }
25
-
26
- mask(): string {
27
- if (this.value.length <= 8) return '********';
28
- return `${this.value.substring(0, 4)}...${this.value.substring(this.value.length - 4)}`;
29
- }
30
25
  }
@@ -26,16 +26,4 @@ export class ModelId {
26
26
  toString(): string {
27
27
  return this.value;
28
28
  }
29
-
30
- isImage(): boolean {
31
- return this.value === 'p-image';
32
- }
33
-
34
- isImageEdit(): boolean {
35
- return this.value === 'p-image-edit';
36
- }
37
-
38
- isVideo(): boolean {
39
- return this.value === 'p-video';
40
- }
41
29
  }
@@ -15,8 +15,4 @@ export class SessionId {
15
15
  toString(): string {
16
16
  return this.value;
17
17
  }
18
-
19
- equals(other: SessionId): boolean {
20
- return this.value === other.value;
21
- }
22
18
  }
package/src/index.ts CHANGED
@@ -52,6 +52,7 @@ export type { PrunaProvider as PrunaProviderType } from "./infrastructure/servic
52
52
  export {
53
53
  cleanupRequestStore,
54
54
  stopAutomaticCleanup,
55
+ cleanupRequestIdMappings,
55
56
  } from "./infrastructure/services/request-store";
56
57
  export type { ActiveRequest } from "./infrastructure/services/request-store";
57
58
 
@@ -102,7 +103,6 @@ export {
102
103
  MIME_AUDIO_WAV,
103
104
  MIME_AUDIO_FLAC,
104
105
  MIME_AUDIO_MP4,
105
- MIME_APPLICATION_OCTET,
106
106
  MIME_DEFAULT,
107
107
  MIME_TO_EXTENSION,
108
108
  getExtensionForMime,
@@ -87,14 +87,3 @@ export class PrunaLogger {
87
87
  // Singleton instance
88
88
  export const logger = PrunaLogger.getInstance();
89
89
 
90
- // Convenience factory function
91
- export function createLogger(tag: string) {
92
- return {
93
- info: (sessionId: string, message: string, data?: Record<string, unknown>) =>
94
- logger.info(sessionId, tag, message, data),
95
- warn: (sessionId: string, message: string, data?: Record<string, unknown>) =>
96
- logger.warn(sessionId, tag, message, data),
97
- error: (sessionId: string, message: string, data?: Record<string, unknown>) =>
98
- logger.error(sessionId, tag, message, data),
99
- };
100
- }
@@ -435,7 +435,10 @@ export async function pollForResult(
435
435
  });
436
436
 
437
437
  if (!statusRes.ok) {
438
- generationLogCollector.warn(sessionId, TAG, `Poll attempt ${i + 1}/${maxAttempts}: HTTP ${statusRes.status}, skipping...`);
438
+ // Only log non-retryable errors to reduce log noise
439
+ if (statusRes.status >= 400 && statusRes.status < 500) {
440
+ generationLogCollector.warn(sessionId, TAG, `Poll HTTP ${statusRes.status}, skipping...`);
441
+ }
439
442
  continue;
440
443
  }
441
444
 
@@ -449,21 +452,20 @@ export async function pollForResult(
449
452
  }
450
453
  } else if (statusData.status === 'failed') {
451
454
  const errorMessage = statusData.error || "Generation failed during processing.";
452
- generationLogCollector.error(sessionId, TAG, `Polling: generation failed ${errorMessage}`);
455
+ generationLogCollector.error(sessionId, TAG, `Polling failed: ${errorMessage}`);
453
456
  throw new Error(errorMessage);
454
457
  }
455
458
 
456
- // Still processing log progress periodically
457
- if ((i + 1) % 10 === 0) {
458
- generationLogCollector.log(sessionId, TAG, `Polling: still processing (attempt ${i + 1}/${maxAttempts})...`);
459
+ // Log progress only every 30 attempts to reduce overhead (every ~90 seconds)
460
+ if ((i + 1) % 30 === 0) {
461
+ generationLogCollector.log(sessionId, TAG, `Still processing (attempt ${i + 1}/${maxAttempts})`);
459
462
  }
460
463
  } catch (error) {
461
464
  if (error instanceof Error && error.message.includes("cancelled by user")) {
462
465
  throw error;
463
466
  }
464
- // Non-fatal poll error — continue polling
467
+ // Non-fatal poll error — continue polling silently
465
468
  if (error instanceof Error && !error.message.includes("failed during processing")) {
466
- generationLogCollector.warn(sessionId, TAG, `Poll attempt ${i + 1} error: ${error.message}`);
467
469
  continue;
468
470
  }
469
471
  throw error;
@@ -478,15 +480,31 @@ export async function pollForResult(
478
480
  * Checks multiple possible locations (priority order).
479
481
  */
480
482
  export function extractUri(data: PrunaPredictionResponse): string | null {
481
- return (
482
- data.generation_url ||
483
- (data.output && typeof data.output === 'object' && !Array.isArray(data.output) ? (data.output as { url: string }).url : null) ||
484
- (typeof data.output === 'string' ? data.output : null) ||
485
- data.video_url ||
486
- (Array.isArray(data.output) ? data.output[0] : null) ||
487
- data.data ||
488
- null
489
- );
483
+ // Priority 1: Direct generation URL
484
+ if (data.generation_url) return data.generation_url;
485
+
486
+ // Priority 2: Output object URL
487
+ if (data.output && typeof data.output === 'object' && !Array.isArray(data.output)) {
488
+ const outputObj = data.output as { url?: string };
489
+ if (outputObj.url) return outputObj.url;
490
+ }
491
+
492
+ // Priority 3: Output as string
493
+ if (typeof data.output === 'string') return data.output;
494
+
495
+ // Priority 4: Video URL
496
+ if (data.video_url) return data.video_url;
497
+
498
+ // Priority 5: First array element
499
+ if (Array.isArray(data.output) && data.output.length > 0) {
500
+ const firstElement = data.output[0];
501
+ if (typeof firstElement === 'string') return firstElement;
502
+ }
503
+
504
+ // Priority 6: Data field
505
+ if (data.data) return data.data;
506
+
507
+ return null;
490
508
  }
491
509
 
492
510
  /**
@@ -300,33 +300,12 @@ export class PrunaProvider implements IAIProvider {
300
300
  }
301
301
  }
302
302
 
303
- reset(): void {
304
- cancelAllRequests();
305
- this.lastRequestKey = null;
306
- this.apiKey = null;
307
- this.initialized = false;
308
- }
309
-
310
303
  cancelCurrentRequest(): void {
311
304
  if (this.lastRequestKey) {
312
305
  cancelRequest(this.lastRequestKey);
313
306
  this.lastRequestKey = null;
314
307
  }
315
308
  }
316
-
317
- hasRunningRequest(): boolean {
318
- return hasActiveRequests();
319
- }
320
-
321
- getSessionLogs(sessionId?: string): LogEntry[] {
322
- if (!sessionId) return [];
323
- return generationLogCollector.getEntries(sessionId);
324
- }
325
-
326
- endLogSession(sessionId?: string): LogEntry[] {
327
- if (!sessionId) return [];
328
- return generationLogCollector.endSession(sessionId);
329
- }
330
309
  }
331
310
 
332
311
  export const prunaProvider = new PrunaProvider();
@@ -57,19 +57,16 @@ function getRequestKeyCache(): RequestKeyCache {
57
57
  return globalObj[REQUEST_KEY_CACHE_KEY] as RequestKeyCache;
58
58
  }
59
59
 
60
- function sortKeys(obj: unknown): unknown {
61
- if (obj === null || typeof obj !== "object") return obj;
62
- if (Array.isArray(obj)) return obj.map(sortKeys);
63
- const sorted: Record<string, unknown> = {};
64
- for (const key of Object.keys(obj as Record<string, unknown>).sort()) {
65
- sorted[key] = sortKeys((obj as Record<string, unknown>)[key]);
66
- }
67
- return sorted;
68
- }
69
-
70
60
  function generateCacheKey(model: string, input: Record<string, unknown>): string {
71
- // Fast hash using JSON.stringify (already sorted by sortKeys)
72
- return `${model}:${JSON.stringify(input)}`;
61
+ // Fast deterministic hash using sorted property names
62
+ // Much faster than deep object sorting + JSON.stringify
63
+ const keys = Object.keys(input).sort();
64
+ let hash = model;
65
+ for (const key of keys) {
66
+ const value = input[key];
67
+ hash += `|${key}:${typeof value === 'object' ? JSON.stringify(value) : String(value)}`;
68
+ }
69
+ return hash;
73
70
  }
74
71
 
75
72
  export function createRequestKey(model: string, input: Record<string, unknown>): string {
@@ -83,20 +80,11 @@ export function createRequestKey(model: string, input: Record<string, unknown>):
83
80
  return cached.key;
84
81
  }
85
82
 
86
- // Cache miss or expired - generate new key
87
- // Use full JSON string instead of hash to eliminate collision risk
88
- // Sort keys ensures consistent key generation regardless of object property order
89
- const inputStr = JSON.stringify(sortKeys(input));
90
-
91
- // Use base64 encoding for safer string representation
92
- // This eliminates collision risk entirely while maintaining readability
93
- const safeInputStr = inputStr.replace(/[^a-zA-Z0-9]/g, '_');
94
-
95
- // Use first 64 chars to keep key length manageable while maintaining uniqueness
96
- const prefix = safeInputStr.substring(0, 64);
97
- const suffix = safeInputStr.length > 64 ? safeInputStr.slice(-64) : '';
98
-
99
- const requestKey = `${model}:${prefix}${suffix ? '...' + suffix : ''}`;
83
+ // Cache miss or expired - generate new key using simple hash
84
+ // Use timestamp + random suffix for uniqueness (crypto-friendly)
85
+ const timestamp = Date.now();
86
+ const randomSuffix = Math.random().toString(36).substring(2, 10);
87
+ const requestKey = `${model}_${cacheKey.substring(0, 16)}_${timestamp}_${randomSuffix}`;
100
88
 
101
89
  // Store in cache with LRU eviction
102
90
  cache.set(cacheKey, { key: requestKey, timestamp: now });
@@ -112,13 +100,6 @@ export function createRequestKey(model: string, input: Record<string, unknown>):
112
100
  return requestKey;
113
101
  }
114
102
 
115
- export function clearRequestKeyCache(): void {
116
- const globalObj = globalThis as Record<string, unknown>;
117
- if (globalObj[REQUEST_KEY_CACHE_KEY]) {
118
- (globalObj[REQUEST_KEY_CACHE_KEY] as RequestKeyCache).clear();
119
- }
120
- }
121
-
122
103
  export function getExistingRequest<T>(key: string): ActiveRequest<T> | undefined {
123
104
  return getRequestStore().get(key) as ActiveRequest<T> | undefined;
124
105
  }
@@ -172,6 +153,9 @@ export function cleanupRequestStore(maxAge: number = MAX_REQUEST_AGE): number {
172
153
  stopCleanupTimer();
173
154
  }
174
155
 
156
+ // Also cleanup orphaned requestId mappings
157
+ cleanupRequestIdMappings(maxAge);
158
+
175
159
  return cleanedCount;
176
160
  }
177
161
 
@@ -229,6 +213,25 @@ export function removeRequestIdMapping(requestId: string): void {
229
213
  getRequestIdMap().delete(requestId);
230
214
  }
231
215
 
216
+ /** Cleanup old requestId mappings to prevent unbounded memory growth */
217
+ export function cleanupRequestIdMappings(maxAge: number = MAX_REQUEST_AGE): number {
218
+ const requestStore = getRequestStore();
219
+ const requestIdMap = getRequestIdMap();
220
+ const now = Date.now();
221
+ let cleanedCount = 0;
222
+
223
+ // Remove mappings for requests that no longer exist or are too old
224
+ for (const [requestId] of requestIdMap.entries()) {
225
+ const request = requestStore.get(requestId);
226
+ if (!request || (now - request.createdAt > maxAge)) {
227
+ requestIdMap.delete(requestId);
228
+ cleanedCount++;
229
+ }
230
+ }
231
+
232
+ return cleanedCount;
233
+ }
234
+
232
235
  // Clear any leftover timer on module load (hot reload safety)
233
236
  if (typeof globalThis !== "undefined") {
234
237
  const existingTimer = getCleanupTimer();
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { httpClient } from "../api/http-client";
7
7
  import { logger } from "../logging/pruna-logger";
8
- import { PRUNA_FILES_URL, UPLOAD_CONFIG } from "../../services/pruna-provider.constants";
8
+ import { PRUNA_FILES_URL, UPLOAD_CONFIG } from "../services/pruna-provider.constants";
9
9
 
10
10
  export class FileStorageService {
11
11
  async uploadFile(
@@ -15,8 +15,7 @@ export const MIME_AUDIO_FLAC = 'audio/flac' as const;
15
15
  export const MIME_AUDIO_MP4 = 'audio/mp4' as const;
16
16
 
17
17
  // ── Fallback ────────────────────────────────────────────────
18
- export const MIME_APPLICATION_OCTET = 'application/octet-stream' as const;
19
- export const MIME_DEFAULT = MIME_APPLICATION_OCTET;
18
+ export const MIME_DEFAULT = 'application/octet-stream' as const;
20
19
 
21
20
  /** Maps MIME type → file extension for upload naming */
22
21
  export const MIME_TO_EXTENSION: Readonly<Record<string, string>> = {
@@ -27,7 +26,7 @@ export const MIME_TO_EXTENSION: Readonly<Record<string, string>> = {
27
26
  [MIME_AUDIO_WAV]: 'wav',
28
27
  [MIME_AUDIO_FLAC]: 'flac',
29
28
  [MIME_AUDIO_MP4]: 'm4a',
30
- [MIME_APPLICATION_OCTET]: 'bin',
29
+ [MIME_DEFAULT]: 'bin',
31
30
  };
32
31
 
33
32
  /**
@@ -30,20 +30,21 @@ const MAX_SESSIONS = 50;
30
30
 
31
31
  class GenerationLogCollector {
32
32
  private sessions = new Map<string, Session>();
33
- private sessionQueue: string[] = []; // FIFO queue for O(1) eviction
33
+ private sessionQueue = new Set<string>(); // O(1) lookup and deletion
34
34
 
35
35
  startSession(): string {
36
- // Evict oldest session if limit exceeded (O(1) operation)
37
- if (this.sessionQueue.length >= MAX_SESSIONS) {
38
- const oldestKey = this.sessionQueue.shift();
36
+ // Evict oldest session if limit exceeded
37
+ if (this.sessionQueue.size >= MAX_SESSIONS) {
38
+ const oldestKey = this.sessionQueue.keys().next().value;
39
39
  if (oldestKey) {
40
40
  this.sessions.delete(oldestKey);
41
+ this.sessionQueue.delete(oldestKey);
41
42
  }
42
43
  }
43
44
 
44
45
  const id = `pruna_session_${++sessionCounter}_${Date.now()}`;
45
46
  this.sessions.set(id, { startTime: Date.now(), entries: [] });
46
- this.sessionQueue.push(id);
47
+ this.sessionQueue.add(id);
47
48
  return id;
48
49
  }
49
50
 
@@ -60,20 +61,17 @@ class GenerationLogCollector {
60
61
  }
61
62
 
62
63
  getEntries(sessionId: string): LogEntry[] {
63
- return [...(this.sessions.get(sessionId)?.entries ?? [])];
64
+ return this.sessions.get(sessionId)?.entries ?? [];
64
65
  }
65
66
 
66
67
  endSession(sessionId: string): LogEntry[] {
67
68
  const session = this.sessions.get(sessionId);
68
69
  if (!session) return [];
69
- const entries = [...session.entries];
70
+ const entries = session.entries;
70
71
  this.sessions.delete(sessionId);
71
72
 
72
- // Remove from queue as well
73
- const queueIndex = this.sessionQueue.indexOf(sessionId);
74
- if (queueIndex !== -1) {
75
- this.sessionQueue.splice(queueIndex, 1);
76
- }
73
+ // Remove from queue as well (O(1) with Set)
74
+ this.sessionQueue.delete(sessionId);
77
75
 
78
76
  return entries;
79
77
  }
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { providerRegistry } from '@umituz/react-native-ai-generation-content';
7
7
  import { prunaProvider } from '../infrastructure/services/pruna-provider';
8
+ import { logger } from '../infrastructure/logging/pruna-logger';
8
9
 
9
10
  /**
10
11
  * InitModule interface (from @umituz/react-native-design-system)
@@ -89,7 +90,9 @@ export function createAiProviderInitModule(
89
90
 
90
91
  return true;
91
92
  } catch (error) {
92
- console.error('[AiProviderInitModule] Pruna initialization failed:', error);
93
+ logger.error('app-init', 'ai-provider', 'Pruna initialization failed', {
94
+ error: error instanceof Error ? error.message : String(error),
95
+ });
93
96
  throw error;
94
97
  }
95
98
  },
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { providerRegistry } from '@umituz/react-native-ai-generation-content';
7
7
  import { prunaProvider } from '../infrastructure/services/pruna-provider';
8
+ import { logger } from '../infrastructure/logging/pruna-logger';
8
9
 
9
10
  /**
10
11
  * Initializes Pruna provider and registers it with providerRegistry in one call.
@@ -33,7 +34,9 @@ export function initializePrunaProvider(config: {
33
34
 
34
35
  return true;
35
36
  } catch (error) {
36
- console.error('[initializePrunaProvider] Initialization failed:', error);
37
+ logger.error('app-init', 'pruna-provider', 'Initialization failed', {
38
+ error: error instanceof Error ? error.message : String(error),
39
+ });
37
40
  throw error;
38
41
  }
39
42
  }
@@ -34,11 +34,7 @@ function convertJobStatusToPrunaQueueStatus(status: JobStatus, currentRequestId:
34
34
  return {
35
35
  status: status.status as PrunaQueueStatus["status"],
36
36
  requestId: status.requestId ?? currentRequestId ?? "",
37
- logs: status.logs?.map((log: AILogEntry) => ({
38
- message: log.message,
39
- level: log.level,
40
- timestamp: log.timestamp,
41
- })),
37
+ logs: status.logs as PrunaQueueStatus["logs"], // Type-safe cast, no array copy
42
38
  };
43
39
  }
44
40
 
package/src/index.new.ts DELETED
@@ -1,65 +0,0 @@
1
- /**
2
- * Pruna AI Provider - Refactored with DDD Architecture
3
- *
4
- * @module react-native-ai-pruna-provider
5
- *
6
- * Architecture:
7
- * - Domain: Business logic, value objects, domain services
8
- * - Application: Use cases, orchestration, DTOs
9
- * - Infrastructure: API clients, storage, logging
10
- * - Presentation: React hooks, UI integration
11
- *
12
- * Key Features:
13
- * - Clean separation of concerns
14
- * - Maximum 150 lines per file
15
- * - No code duplication
16
- * - Type-safe with TypeScript
17
- * - Comprehensive error handling
18
- */
19
-
20
- // Core Service
21
- export { prunaService, PrunaService } from './application/services/pruna-service';
22
- export type {
23
- PrunaConfig,
24
- PrunaModel,
25
- } from './application/services/pruna-service';
26
-
27
- // React Hooks
28
- export { usePrunaGeneration } from './presentation/hooks/use-pruna-generation.new';
29
- export type { PrunaGenerationState } from './presentation/hooks/use-pruna-generation.new';
30
-
31
- // DTOs
32
- export type {
33
- PrunaImageGenerationRequest,
34
- PrunaVideoGenerationRequest,
35
- PrunaImageEditRequest,
36
- PrunaGenerationResponse,
37
- PrunaGenerationOptions,
38
- PrunaGenerationError,
39
- } from './application/dto/pruna.dto';
40
-
41
- // Domain Layer
42
- export { SessionId } from './domain/value-objects/session-id.value';
43
- export { ApiKey } from './domain/value-objects/api-key.value';
44
- export { ModelId, PrunaModelId } from './domain/value-objects/model-id.value';
45
- export { ValidationService, ValidationResult } from './domain/services/validation.domain-service';
46
- export { ErrorMapperService } from './domain/services/error-mapper.domain-service';
47
-
48
- // Infrastructure Layer
49
- export { logger, createLogger, LogLevel } from './infrastructure/logging/pruna-logger';
50
- export { httpClient, HttpClient } from './infrastructure/api/http-client';
51
- export { fileStorageService, FileStorageService } from './infrastructure/storage/file-storage';
52
-
53
- // Use Cases (for advanced usage)
54
- export { generateImageUseCase, GenerateImageUseCase } from './application/use-cases/generate-image.use-case';
55
- export { generateVideoUseCase, GenerateVideoUseCase } from './application/use-cases/generate-video.use-case';
56
- export { generateImageEditUseCase, GenerateImageEditUseCase } from './application/use-cases/generate-image-edit.use-case';
57
-
58
- // Legacy Exports (for backward compatibility)
59
- export { prunaProvider as PrunaProvider } from './infrastructure/services/pruna-provider';
60
- export type {
61
- IAIProvider,
62
- AIProviderConfig,
63
- SubscribeOptions,
64
- RunOptions,
65
- } from './domain/types';
@@ -1,182 +0,0 @@
1
- /**
2
- * Pruna Generation Hook (Refactored)
3
- * Clean React hook using new DDD architecture
4
- */
5
-
6
- import { useState, useCallback, useRef } from 'react';
7
- import { prunaService } from '../../application/services/pruna-service';
8
- import type {
9
- PrunaImageGenerationRequest,
10
- PrunaVideoGenerationRequest,
11
- PrunaImageEditRequest,
12
- PrunaGenerationResponse,
13
- PrunaGenerationError,
14
- } from '../../application/dto/pruna.dto';
15
-
16
- export interface PrunaGenerationState {
17
- isGenerating: boolean;
18
- progress: number;
19
- status: string;
20
- error: PrunaGenerationError | null;
21
- result: PrunaGenerationResponse | null;
22
- }
23
-
24
- export function usePrunaGeneration() {
25
- const [state, setState] = useState<PrunaGenerationState>({
26
- isGenerating: false,
27
- progress: 0,
28
- status: 'idle',
29
- error: null,
30
- result: null,
31
- });
32
-
33
- const abortControllerRef = useRef<AbortController | null>(null);
34
-
35
- const generateImage = useCallback(async (request: PrunaImageGenerationRequest) => {
36
- const controller = new AbortController();
37
- abortControllerRef.current = controller;
38
-
39
- setState({
40
- isGenerating: true,
41
- progress: 0,
42
- status: 'starting',
43
- error: null,
44
- result: null,
45
- });
46
-
47
- try {
48
- const response = await prunaService.generateImage(request, controller.signal);
49
- setState({
50
- isGenerating: false,
51
- progress: 100,
52
- status: 'completed',
53
- error: null,
54
- result: response,
55
- });
56
- return response;
57
- } catch (error) {
58
- const errorObj: PrunaGenerationError = {
59
- type: 'unknown',
60
- message: error instanceof Error ? error.message : String(error),
61
- retryable: false,
62
- originalError: error instanceof Error ? error.stack : undefined,
63
- };
64
- setState({
65
- isGenerating: false,
66
- progress: 0,
67
- status: 'error',
68
- error: errorObj,
69
- result: null,
70
- });
71
- throw error;
72
- }
73
- }, []);
74
-
75
- const generateVideo = useCallback(async (request: PrunaVideoGenerationRequest) => {
76
- const controller = new AbortController();
77
- abortControllerRef.current = controller;
78
-
79
- setState({
80
- isGenerating: true,
81
- progress: 0,
82
- status: 'starting',
83
- error: null,
84
- result: null,
85
- });
86
-
87
- try {
88
- const response = await prunaService.generateVideo(request, controller.signal);
89
- setState({
90
- isGenerating: false,
91
- progress: 100,
92
- status: 'completed',
93
- error: null,
94
- result: response,
95
- });
96
- return response;
97
- } catch (error) {
98
- const errorObj: PrunaGenerationError = {
99
- type: 'unknown',
100
- message: error instanceof Error ? error.message : String(error),
101
- retryable: false,
102
- originalError: error instanceof Error ? error.stack : undefined,
103
- };
104
- setState({
105
- isGenerating: false,
106
- progress: 0,
107
- status: 'error',
108
- error: errorObj,
109
- result: null,
110
- });
111
- throw error;
112
- }
113
- }, []);
114
-
115
- const generateImageEdit = useCallback(async (request: PrunaImageEditRequest) => {
116
- const controller = new AbortController();
117
- abortControllerRef.current = controller;
118
-
119
- setState({
120
- isGenerating: true,
121
- progress: 0,
122
- status: 'starting',
123
- error: null,
124
- result: null,
125
- });
126
-
127
- try {
128
- const response = await prunaService.generateImageEdit(request, controller.signal);
129
- setState({
130
- isGenerating: false,
131
- progress: 100,
132
- status: 'completed',
133
- error: null,
134
- result: response,
135
- });
136
- return response;
137
- } catch (error) {
138
- const errorObj: PrunaGenerationError = {
139
- type: 'unknown',
140
- message: error instanceof Error ? error.message : String(error),
141
- retryable: false,
142
- originalError: error instanceof Error ? error.stack : undefined,
143
- };
144
- setState({
145
- isGenerating: false,
146
- progress: 0,
147
- status: 'error',
148
- error: errorObj,
149
- result: null,
150
- });
151
- throw error;
152
- }
153
- }, []);
154
-
155
- const cancel = useCallback(() => {
156
- abortControllerRef.current?.abort();
157
- setState(prev => ({
158
- ...prev,
159
- isGenerating: false,
160
- status: 'cancelled',
161
- }));
162
- }, []);
163
-
164
- const reset = useCallback(() => {
165
- setState({
166
- isGenerating: false,
167
- progress: 0,
168
- status: 'idle',
169
- error: null,
170
- result: null,
171
- });
172
- }, []);
173
-
174
- return {
175
- state,
176
- generateImage,
177
- generateVideo,
178
- generateImageEdit,
179
- cancel,
180
- reset,
181
- };
182
- }