@plasius/ai 1.1.4 → 1.1.6

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.
Files changed (98) hide show
  1. package/CHANGELOG.md +22 -2
  2. package/README.md +101 -2
  3. package/dist/components/pixelverse/balance.d.ts +6 -2
  4. package/dist/components/pixelverse/balance.d.ts.map +1 -1
  5. package/dist/components/pixelverse/balance.js +13 -23
  6. package/dist/components/pixelverse/index.d.ts +1 -1
  7. package/dist/components/pixelverse/index.d.ts.map +1 -1
  8. package/dist/components/pixelverse/index.js +1 -1
  9. package/dist/components/pixelverse/video-generation-editor.d.ts +10 -0
  10. package/dist/components/pixelverse/video-generation-editor.d.ts.map +1 -0
  11. package/dist/components/pixelverse/video-generation-editor.js +79 -0
  12. package/dist/platform/adapter-platform.d.ts +60 -0
  13. package/dist/platform/adapter-platform.d.ts.map +1 -0
  14. package/dist/platform/adapter-platform.js +222 -0
  15. package/dist/platform/gemini-adapter.d.ts +15 -0
  16. package/dist/platform/gemini-adapter.d.ts.map +1 -0
  17. package/dist/platform/gemini-adapter.js +293 -0
  18. package/dist/platform/http-resilience.d.ts +19 -0
  19. package/dist/platform/http-resilience.d.ts.map +1 -0
  20. package/dist/platform/http-resilience.js +126 -0
  21. package/dist/platform/index.d.ts +22 -1
  22. package/dist/platform/index.d.ts.map +1 -1
  23. package/dist/platform/index.js +24 -0
  24. package/dist/platform/openai-adapter.d.ts +24 -0
  25. package/dist/platform/openai-adapter.d.ts.map +1 -0
  26. package/dist/platform/openai-adapter.js +398 -0
  27. package/dist/platform/video-provider-adapter.d.ts +54 -0
  28. package/dist/platform/video-provider-adapter.d.ts.map +1 -0
  29. package/dist/platform/video-provider-adapter.js +165 -0
  30. package/dist/platform/video-provider-platform.d.ts +13 -0
  31. package/dist/platform/video-provider-platform.d.ts.map +1 -0
  32. package/dist/platform/video-provider-platform.js +102 -0
  33. package/dist-cjs/components/pixelverse/balance.d.ts +6 -2
  34. package/dist-cjs/components/pixelverse/balance.d.ts.map +1 -1
  35. package/dist-cjs/components/pixelverse/balance.js +13 -23
  36. package/dist-cjs/components/pixelverse/index.d.ts +1 -1
  37. package/dist-cjs/components/pixelverse/index.d.ts.map +1 -1
  38. package/dist-cjs/components/pixelverse/index.js +1 -1
  39. package/dist-cjs/components/pixelverse/video-generation-editor.d.ts +10 -0
  40. package/dist-cjs/components/pixelverse/video-generation-editor.d.ts.map +1 -0
  41. package/dist-cjs/components/pixelverse/video-generation-editor.js +85 -0
  42. package/dist-cjs/platform/adapter-platform.d.ts +60 -0
  43. package/dist-cjs/platform/adapter-platform.d.ts.map +1 -0
  44. package/dist-cjs/platform/adapter-platform.js +225 -0
  45. package/dist-cjs/platform/gemini-adapter.d.ts +15 -0
  46. package/dist-cjs/platform/gemini-adapter.d.ts.map +1 -0
  47. package/dist-cjs/platform/gemini-adapter.js +296 -0
  48. package/dist-cjs/platform/http-resilience.d.ts +19 -0
  49. package/dist-cjs/platform/http-resilience.d.ts.map +1 -0
  50. package/dist-cjs/platform/http-resilience.js +129 -0
  51. package/dist-cjs/platform/index.d.ts +22 -1
  52. package/dist-cjs/platform/index.d.ts.map +1 -1
  53. package/dist-cjs/platform/index.js +30 -1
  54. package/dist-cjs/platform/openai-adapter.d.ts +24 -0
  55. package/dist-cjs/platform/openai-adapter.d.ts.map +1 -0
  56. package/dist-cjs/platform/openai-adapter.js +401 -0
  57. package/dist-cjs/platform/video-provider-adapter.d.ts +54 -0
  58. package/dist-cjs/platform/video-provider-adapter.d.ts.map +1 -0
  59. package/dist-cjs/platform/video-provider-adapter.js +168 -0
  60. package/dist-cjs/platform/video-provider-platform.d.ts +13 -0
  61. package/dist-cjs/platform/video-provider-platform.d.ts.map +1 -0
  62. package/dist-cjs/platform/video-provider-platform.js +105 -0
  63. package/docs/api-reference.md +59 -0
  64. package/docs/architecture.md +5 -1
  65. package/docs/providers.md +24 -6
  66. package/package.json +6 -6
  67. package/src/components/pixelverse/balance.tsx +22 -35
  68. package/src/components/pixelverse/index.ts +1 -1
  69. package/src/components/pixelverse/video-generation-editor.tsx +164 -0
  70. package/src/platform/adapter-platform.ts +440 -0
  71. package/src/platform/gemini-adapter.ts +391 -0
  72. package/src/platform/http-resilience.ts +198 -0
  73. package/src/platform/index.ts +68 -0
  74. package/src/platform/openai-adapter.ts +552 -0
  75. package/src/platform/video-provider-adapter.ts +303 -0
  76. package/src/platform/video-provider-platform.ts +208 -0
  77. package/dist/components/pixelverse/pixelverseeditor.d.ts +0 -16
  78. package/dist/components/pixelverse/pixelverseeditor.d.ts.map +0 -1
  79. package/dist/components/pixelverse/pixelverseeditor.js +0 -21
  80. package/dist/platform/openai.d.ts +0 -8
  81. package/dist/platform/openai.d.ts.map +0 -1
  82. package/dist/platform/openai.js +0 -61
  83. package/dist/platform/pixelverse.d.ts +0 -6
  84. package/dist/platform/pixelverse.d.ts.map +0 -1
  85. package/dist/platform/pixelverse.js +0 -196
  86. package/dist-cjs/components/pixelverse/pixelverseeditor.d.ts +0 -16
  87. package/dist-cjs/components/pixelverse/pixelverseeditor.d.ts.map +0 -1
  88. package/dist-cjs/components/pixelverse/pixelverseeditor.js +0 -27
  89. package/dist-cjs/platform/openai.d.ts +0 -8
  90. package/dist-cjs/platform/openai.d.ts.map +0 -1
  91. package/dist-cjs/platform/openai.js +0 -67
  92. package/dist-cjs/platform/pixelverse.d.ts +0 -6
  93. package/dist-cjs/platform/pixelverse.d.ts.map +0 -1
  94. package/dist-cjs/platform/pixelverse.js +0 -199
  95. package/src/components/pixelverse/pixelverseeditor.mocule.css +0 -0
  96. package/src/components/pixelverse/pixelverseeditor.tsx +0 -74
  97. package/src/platform/openai.ts +0 -123
  98. package/src/platform/pixelverse.ts +0 -309
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createVideoProviderPlatform = createVideoProviderPlatform;
4
+ const node_perf_hooks_1 = require("node:perf_hooks");
5
+ function createCompletionBase(type, model, requestor, durationMs) {
6
+ return {
7
+ partitionKey: requestor,
8
+ id: crypto.randomUUID(),
9
+ type,
10
+ model,
11
+ createdAt: new Date().toISOString(),
12
+ durationMs,
13
+ usage: {},
14
+ };
15
+ }
16
+ async function waitForCompletion(adapter, videoId, apiKey, maxRetries, delayMs) {
17
+ for (let attempt = 0; attempt < maxRetries; attempt += 1) {
18
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
19
+ const result = await adapter.getVideoResult(videoId, {
20
+ apiKey,
21
+ traceId: crypto.randomUUID(),
22
+ });
23
+ if (result.state === "completed" && result.videoUrl) {
24
+ return result.videoUrl;
25
+ }
26
+ if (result.state === "failed") {
27
+ throw new Error("Video generation failed in provider adapter.");
28
+ }
29
+ }
30
+ throw new Error("Timed out waiting for provider video completion.");
31
+ }
32
+ async function createVideoProviderPlatform(userId, props) {
33
+ const apiKey = props.apiKey.trim();
34
+ if (!apiKey) {
35
+ throw new Error("apiKey is required for createVideoProviderPlatform.");
36
+ }
37
+ const maxRetries = props.polling?.maxRetries ?? 20;
38
+ const delayMs = props.polling?.delayMs ?? 3000;
39
+ const chatWithAI = (_userId, _input, _context, _model) => {
40
+ return Promise.reject(new Error("Not implemented"));
41
+ };
42
+ const synthesizeSpeech = (_userId, _input, _voice, _context, _model) => {
43
+ return Promise.reject(new Error("Not implemented"));
44
+ };
45
+ const transcribeSpeech = (_userId, _input, _context, _model) => {
46
+ return Promise.reject(new Error("Not implemented"));
47
+ };
48
+ const generateImage = (_userId, _input, _context, _model) => {
49
+ return Promise.reject(new Error("Not implemented"));
50
+ };
51
+ const produceVideo = async (requestorId, prompt, image, _context, model) => {
52
+ const startedAt = node_perf_hooks_1.performance.now();
53
+ const uploaded = await props.adapter.uploadImage(image, {
54
+ apiKey,
55
+ traceId: crypto.randomUUID(),
56
+ });
57
+ const generated = await props.adapter.generateVideo({
58
+ imageId: uploaded.imageId,
59
+ prompt,
60
+ ...props.defaultVideoRequest,
61
+ }, {
62
+ apiKey,
63
+ traceId: crypto.randomUUID(),
64
+ });
65
+ const videoUrl = await waitForCompletion(props.adapter, generated.videoId, apiKey, maxRetries, delayMs);
66
+ const durationMs = node_perf_hooks_1.performance.now() - startedAt;
67
+ const base = createCompletionBase("video", model, requestorId, durationMs);
68
+ return {
69
+ ...base,
70
+ url: new URL(videoUrl),
71
+ };
72
+ };
73
+ const generateModel = (_userId, _input, _context, _model) => {
74
+ return Promise.reject(new Error("Not implemented"));
75
+ };
76
+ const checkBalance = async (requestorId) => {
77
+ const startedAt = node_perf_hooks_1.performance.now();
78
+ const providerBalance = props.adapter.getBalance
79
+ ? await props.adapter.getBalance({
80
+ apiKey,
81
+ traceId: crypto.randomUUID(),
82
+ })
83
+ : {
84
+ monthlyCredit: 0,
85
+ packageCredit: 0,
86
+ };
87
+ const durationMs = node_perf_hooks_1.performance.now() - startedAt;
88
+ const base = createCompletionBase("balanceCompletion", "", requestorId, durationMs);
89
+ return {
90
+ ...base,
91
+ balance: providerBalance.monthlyCredit + providerBalance.packageCredit,
92
+ };
93
+ };
94
+ const currentBalance = (await checkBalance(userId)).balance;
95
+ return {
96
+ chatWithAI,
97
+ synthesizeSpeech,
98
+ transcribeSpeech,
99
+ generateImage,
100
+ produceVideo,
101
+ generateModel,
102
+ checkBalance,
103
+ currentBalance,
104
+ };
105
+ }
@@ -12,6 +12,7 @@ Routing capability enum:
12
12
  - `Image`
13
13
  - `Video`
14
14
  - `Balance`
15
+ - `Model`
15
16
 
16
17
  ## Core Interfaces
17
18
 
@@ -24,6 +25,7 @@ Contract for runtime adapters:
24
25
  - `transcribeSpeech(userId, input, context, model)`
25
26
  - `generateImage(userId, input, context, model)`
26
27
  - `produceVideo(userId, input, image, context, model)`
28
+ - `generateModel(userId, input, context, model)`
27
29
  - `checkBalance(userId)`
28
30
  - `currentBalance`
29
31
 
@@ -51,8 +53,64 @@ Specialized variants:
51
53
  - `ImageCompletion`
52
54
  - `SpeechCompletion`
53
55
  - `VideoCompletion`
56
+ - `ModelCompletion`
54
57
  - `BalanceCompletion`
55
58
 
59
+ ## Adapter Composition
60
+
61
+ ### `AICapabilityAdapter`
62
+
63
+ Provider adapter contract with capability declarations and optional operation handlers:
64
+
65
+ - `id`
66
+ - `capabilities`
67
+ - `chatWithAI(request)?`
68
+ - `synthesizeSpeech(request)?`
69
+ - `transcribeSpeech(request)?`
70
+ - `generateImage(request)?`
71
+ - `produceVideo(request)?`
72
+ - `generateModel(request)?`
73
+ - `checkBalance(request)?`
74
+
75
+ ### `createAdapterPlatform(userId, props)`
76
+
77
+ Builds an `AIPlatform` by routing capability calls to registered adapters and injecting developer-supplied API keys from `props.apiKeys`.
78
+
79
+ ### `HttpClientPolicy`
80
+
81
+ Shared HTTP resilience policy used by built-in adapters:
82
+
83
+ - `maxAttempts?`
84
+ - `timeoutMs?`
85
+ - `baseDelayMs?`
86
+ - `maxDelayMs?`
87
+ - `jitterRatio?`
88
+ - `respectRetryAfter?`
89
+ - `retryableMethods?`
90
+ - `retryableStatusCodes?`
91
+
92
+ ### `createOpenAIAdapter(options?)`
93
+
94
+ Creates a built-in OpenAI adapter implementing:
95
+
96
+ - `chatWithAI`
97
+ - `synthesizeSpeech`
98
+ - `transcribeSpeech`
99
+ - `generateImage`
100
+ - `generateModel`
101
+
102
+ `options.httpPolicy` applies retry/timeout behavior to all OpenAI adapter HTTP requests.
103
+
104
+ ### `createGeminiAdapter(options?)`
105
+
106
+ Creates a built-in Gemini adapter implementing:
107
+
108
+ - `chatWithAI`
109
+ - `generateImage`
110
+ - `generateModel`
111
+
112
+ `options.httpPolicy` applies retry/timeout behavior to all Gemini adapter HTTP requests.
113
+
56
114
  ## Exported Schemas
57
115
 
58
116
  - `completionSchema`
@@ -61,4 +119,5 @@ Specialized variants:
61
119
  - `imageCompletionSchema`
62
120
  - `speechCompletionSchema`
63
121
  - `videoCompletionSchema`
122
+ - `modelCompletionSchema`
64
123
  - `balanceCompletionSchema`
@@ -4,8 +4,11 @@
4
4
 
5
5
  - **Capability model**: `AICapability` describes the logical feature set.
6
6
  - **Platform contract**: `AIPlatform` defines the async operations required by a runtime adapter.
7
- - **Completion contracts**: typed completion outputs normalize chat/text/image/speech/video/balance results.
7
+ - **Completion contracts**: typed completion outputs normalize chat/text/image/speech/video/model/balance results.
8
8
  - **Schema layer**: schema definitions ensure consistent metadata and persistence shape across runtimes.
9
+ - **Adapter composition**: `createAdapterPlatform` routes capabilities across multiple adapters and injects API keys supplied by the package consumer.
10
+ - **Built-in providers**: OpenAI and Gemini adapters are included for out-of-the-box capability wiring.
11
+ - **Resilient transport**: adapters use a shared HTTP resilience policy (timeouts, exponential backoff + jitter, and `Retry-After` handling).
9
12
 
10
13
  ## Design Intention
11
14
 
@@ -14,6 +17,7 @@ This package is not tied to a single provider runtime. Host applications should:
14
17
  1. Implement or wrap an `AIPlatform` adapter.
15
18
  2. Keep provider credentials and network execution outside this package boundary.
16
19
  3. Emit normalized completion objects based on the exported contract types.
20
+ 4. Register per-provider API keys through `AdapterPlatformProps.apiKeys`.
17
21
 
18
22
  ## Current Boundaries
19
23
 
package/docs/providers.md CHANGED
@@ -5,22 +5,40 @@ specific provider implementation.
5
5
 
6
6
  ## Recommended Integration Pattern
7
7
 
8
- 1. Build provider adapters in your host app or a dedicated integration package.
9
- 2. Implement `AIPlatform` for each provider.
8
+ 1. Use built-in adapters (`createOpenAIAdapter`, `createGeminiAdapter`) or build provider adapters in your host app.
9
+ 2. Compose adapters with `createAdapterPlatform(...)` and an `apiKeys` map keyed by adapter id.
10
10
  3. Keep provider secrets in runtime environment variables, not in UI bundles.
11
11
  4. Normalize results into exported completion models.
12
12
 
13
+ ## Built-In Adapters
14
+
15
+ - `createOpenAIAdapter(options?)`
16
+ - Default adapter id: `openai`
17
+ - Capabilities: chat, speech synthesis, transcription, image generation, model generation
18
+ - `createGeminiAdapter(options?)`
19
+ - Default adapter id: `gemini`
20
+ - Capabilities: chat, image generation, model generation
21
+
22
+ ## Network Citizenship Defaults
23
+
24
+ Built-in adapters and the generic HTTP video adapter apply resilient request behavior by default:
25
+
26
+ - Timeout per request (`30s` default).
27
+ - Exponential backoff with jitter for transient failures.
28
+ - Retry on common transient statuses (`408`, `409`, `425`, `429`, `500`, `502`, `503`, `504`).
29
+ - Honor `Retry-After` when present.
30
+
31
+ Override these values using `httpPolicy` in adapter options.
32
+
13
33
  ## Adapter Checklist
14
34
 
15
35
  - Map provider-specific response IDs to `Completion.id`.
16
36
  - Populate `durationMs` from measured execution time.
17
37
  - Set `partitionKey` to your stable user/session key.
18
38
  - Attach provider usage/cost metadata to `usage`.
39
+ - Use stable adapter ids and provide matching API keys in `AdapterPlatformProps.apiKeys`.
19
40
  - Return strongly typed completion variants for each capability.
20
41
 
21
42
  ## Stability Notes
22
43
 
23
- - `src/platform/openai.ts` and `src/platform/pixelverse.ts` are implementation
24
- work areas and should be treated as provisional until explicit public export
25
- and API freeze.
26
- - Prefer composition from host apps until adapter APIs are finalized.
44
+ - Prefer `createOpenAIAdapter` and `createGeminiAdapter` for out-of-the-box provider integration.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plasius/ai",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "Plasius AI functions providing chatbot, text-to-speech, speech-to-text, and AI-generated images and videos",
5
5
  "keywords": [
6
6
  "chatbot",
@@ -31,7 +31,9 @@
31
31
  "build:cjs": "tsc -p tsconfig.json --module commonjs --moduleResolution node --outDir dist-cjs --tsBuildInfoFile dist-cjs/tsconfig.tsbuildinfo",
32
32
  "lint": "eslint .",
33
33
  "prepare": "npm run build",
34
- "demo:run": "npm run build && node demo/example.mjs"
34
+ "demo:run": "npm run build && node demo/example.mjs",
35
+ "pack:check": "node scripts/verify-public-package.cjs",
36
+ "prepublishOnly": "npm run build && npm run pack:check"
35
37
  },
36
38
  "author": "Plasius LTD <development@plasius.co.uk>",
37
39
  "license": "MIT",
@@ -42,7 +44,6 @@
42
44
  "@plasius/schema": "^1.2.1"
43
45
  },
44
46
  "peerDependencies": {
45
- "openai": "^5.23.2",
46
47
  "react": "^19.1.0"
47
48
  },
48
49
  "devDependencies": {
@@ -56,9 +57,8 @@
56
57
  "ajv": "^6.12.6",
57
58
  "eslint": "^10.0.1",
58
59
  "npm-run-all": "^1.1.3",
59
- "openai": "^5.23.2",
60
- "react": "19.2.4",
61
- "react-dom": "19.2.4",
60
+ "react": "^19.1.0",
61
+ "react-dom": "^19.1.0",
62
62
  "react-router-dom": "^7.13.0",
63
63
  "tsx": "^4.21.0",
64
64
  "typescript": "^5.9.3",
@@ -1,61 +1,48 @@
1
1
  import { useEffect, useState } from "react";
2
- import { v4 as uuidv4 } from "uuid";
2
+ import type { ProviderBalance, VideoProviderAdapter } from "../../platform/video-provider-adapter.js";
3
3
  import styles from "./balance.module.css";
4
4
 
5
- interface BalanceResponse {
6
- account_id: number;
7
- credit_monthly: number;
8
- credit_package: number;
5
+ export interface BalanceProps {
6
+ apiKey: string;
7
+ adapter: VideoProviderAdapter;
8
+ refreshMs?: number;
9
9
  }
10
10
 
11
- interface BalanceApiResponse {
12
- Resp?: BalanceResponse;
13
- }
14
-
15
- export default function Balance({ apiKey }: { apiKey: string }) {
16
- const [balance, setBalance] = useState<BalanceResponse | null>(null);
11
+ export default function Balance({ apiKey, adapter, refreshMs = 600000 }: BalanceProps) {
12
+ const [balance, setBalance] = useState<ProviderBalance | null>(null);
17
13
 
18
14
  const fetchBalance = async (): Promise<void> => {
15
+ if (!adapter.getBalance) {
16
+ setBalance({ monthlyCredit: 0, packageCredit: 0 });
17
+ return;
18
+ }
19
+
19
20
  try {
20
- const response = await fetch("/pixelapi/openapi/v2/account/balance", {
21
- method: "GET",
22
- headers: {
23
- "API-KEY": apiKey,
24
- "AI-trace-ID": uuidv4(),
25
- Accept: "application/json",
26
- "Content-Type": "application/json",
27
- },
28
- referrerPolicy: "no-referrer",
21
+ const value = await adapter.getBalance({
22
+ apiKey,
23
+ traceId: crypto.randomUUID(),
29
24
  });
30
-
31
- if (!response.ok) {
32
- console.error("Failed to fetch balance:", response.status, response.statusText);
33
- return;
34
- }
35
-
36
- const data = (await response.json()) as unknown as BalanceApiResponse;
37
- if (data?.Resp) {
38
- setBalance(data.Resp);
39
- }
25
+ setBalance(value);
40
26
  } catch (err) {
41
27
  console.error("fetchBalance() error", err);
42
28
  }
43
29
  };
44
30
 
45
31
  useEffect(() => {
46
- void fetchBalance(); // initial load
32
+ void fetchBalance();
47
33
  const intervalId = setInterval(() => {
48
34
  void fetchBalance();
49
- }, 600000);
35
+ }, refreshMs);
36
+
50
37
  return () => clearInterval(intervalId);
51
- }, [apiKey]);
38
+ }, [apiKey, adapter, refreshMs]);
52
39
 
53
40
  return (
54
41
  <div className={styles.balance_container}>
55
42
  {balance ? (
56
43
  <div>
57
- <p>Monthly Credit: {balance.credit_monthly}</p>
58
- <p>Package Credit: {balance.credit_package}</p>
44
+ <p>Monthly Credit: {balance.monthlyCredit}</p>
45
+ <p>Package Credit: {balance.packageCredit}</p>
59
46
  </div>
60
47
  ) : (
61
48
  <p>Loading balance...</p>
@@ -1,2 +1,2 @@
1
1
  export * from "./balance.js";
2
- export * from "./pixelverseeditor.js";
2
+ export * from "./video-generation-editor.js";
@@ -0,0 +1,164 @@
1
+ import React, { useState } from "react";
2
+ import Balance from "./balance.js";
3
+ import type {
4
+ VideoGenerationRequest,
5
+ VideoProviderAdapter,
6
+ } from "../../platform/video-provider-adapter.js";
7
+
8
+ export interface VideoGenerationEditorProps {
9
+ apiKey: string;
10
+ adapter: VideoProviderAdapter;
11
+ onVideoGenerated?: (videoUrl: string) => void;
12
+ initialRequest?: Partial<Omit<VideoGenerationRequest, "imageId">>;
13
+ }
14
+
15
+ const defaultRequest: Omit<VideoGenerationRequest, "imageId"> = {
16
+ prompt: "",
17
+ model: "standard",
18
+ motionMode: "normal",
19
+ quality: "720p",
20
+ durationSeconds: 5,
21
+ watermark: false,
22
+ };
23
+
24
+ function toRequest(
25
+ overrides?: Partial<Omit<VideoGenerationRequest, "imageId">>
26
+ ): Omit<VideoGenerationRequest, "imageId"> {
27
+ return {
28
+ ...defaultRequest,
29
+ ...overrides,
30
+ };
31
+ }
32
+
33
+ async function waitForVideoCompletion(
34
+ adapter: VideoProviderAdapter,
35
+ videoId: number,
36
+ apiKey: string,
37
+ maxRetries = 20,
38
+ delayMs = 3000
39
+ ): Promise<string> {
40
+ for (let attempt = 0; attempt < maxRetries; attempt += 1) {
41
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
42
+ const result = await adapter.getVideoResult(videoId, {
43
+ apiKey,
44
+ traceId: crypto.randomUUID(),
45
+ });
46
+
47
+ if (result.state === "completed" && result.videoUrl) {
48
+ return result.videoUrl;
49
+ }
50
+
51
+ if (result.state === "failed") {
52
+ throw new Error("Video generation failed.");
53
+ }
54
+ }
55
+
56
+ throw new Error("Timed out waiting for video generation result.");
57
+ }
58
+
59
+ export function VideoGenerationEditor({
60
+ apiKey,
61
+ adapter,
62
+ onVideoGenerated,
63
+ initialRequest,
64
+ }: VideoGenerationEditorProps) {
65
+ const [videoUrl, setVideoUrl] = useState("");
66
+ const [selectedFile, setSelectedFile] = useState<File | null>(null);
67
+ const [loading, setLoading] = useState(false);
68
+ const [videoReady, setVideoReady] = useState(false);
69
+ const [request, setRequest] = useState<Omit<VideoGenerationRequest, "imageId">>(
70
+ toRequest(initialRequest)
71
+ );
72
+
73
+ const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
74
+ const file = event.target.files?.[0] ?? null;
75
+ setSelectedFile(file);
76
+ };
77
+
78
+ const handleRegenerate = () => {
79
+ void handleUploadProcess();
80
+ };
81
+
82
+ const handleUploadProcess = async () => {
83
+ if (!selectedFile) {
84
+ return;
85
+ }
86
+
87
+ setLoading(true);
88
+ setVideoReady(false);
89
+
90
+ try {
91
+ const uploaded = await adapter.uploadImage(selectedFile, {
92
+ apiKey,
93
+ traceId: crypto.randomUUID(),
94
+ });
95
+
96
+ const generated = await adapter.generateVideo(
97
+ {
98
+ ...request,
99
+ imageId: uploaded.imageId,
100
+ },
101
+ {
102
+ apiKey,
103
+ traceId: crypto.randomUUID(),
104
+ }
105
+ );
106
+
107
+ const generatedUrl = await waitForVideoCompletion(adapter, generated.videoId, apiKey);
108
+ setVideoUrl(generatedUrl);
109
+ setVideoReady(true);
110
+ onVideoGenerated?.(generatedUrl);
111
+ } finally {
112
+ setLoading(false);
113
+ }
114
+ };
115
+
116
+ return (
117
+ <div>
118
+ <Balance apiKey={apiKey} adapter={adapter} />
119
+ {!videoReady && !selectedFile && (
120
+ <div>
121
+ <p>Drag/Drop or Click HERE to upload</p>
122
+ <input
123
+ title="Upload Image"
124
+ type="file"
125
+ accept=".jpg,.jpeg,.png,.webp"
126
+ onChange={handleFileChange}
127
+ />
128
+ </div>
129
+ )}
130
+
131
+ {!videoReady ? (
132
+ <div>
133
+ <label>
134
+ Prompt
135
+ <textarea
136
+ value={request.prompt}
137
+ onChange={(event) =>
138
+ setRequest((previous) => ({
139
+ ...previous,
140
+ prompt: event.target.value,
141
+ }))
142
+ }
143
+ />
144
+ </label>
145
+ </div>
146
+ ) : null}
147
+
148
+ {loading && <div>Loading...</div>}
149
+
150
+ {!videoReady && selectedFile && !loading && (
151
+ <button onClick={handleUploadProcess}>Start Upload</button>
152
+ )}
153
+
154
+ {videoReady && (
155
+ <div>
156
+ <video src={videoUrl} controls />
157
+ <button onClick={handleRegenerate}>Regenerate</button>
158
+ </div>
159
+ )}
160
+ </div>
161
+ );
162
+ }
163
+
164
+ export default VideoGenerationEditor;