@j-o-r/hello-dave 0.1.1 → 0.1.4
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/CHANGELOG.md +42 -25
- package/README.md +81 -221
- package/TODO.md +173 -35
- package/agents/agent_creator.js +105 -0
- package/agents/agent_creator.prompt.md +371 -0
- package/agents/ask_agent.js +64 -127
- package/agents/claude_agent.js +68 -0
- package/agents/code_agent.js +55 -135
- package/agents/code_agent.prompt.md +50 -0
- package/agents/echo_agent.js +76 -0
- package/agents/financial_expert.js +75 -0
- package/agents/gpt_agent.js +52 -103
- package/agents/gpt_code.js +81 -0
- package/agents/grok_agent.js +58 -114
- package/agents/minimax_agent.js +92 -0
- package/agents/mureka_agent.js +77 -0
- package/agents/planner_agent.js +172 -0
- package/agents/stability_agent.js +87 -0
- package/agents/test_agent.js +75 -157
- package/agents/weather_agent.js +73 -0
- package/agents/workflow_agent.js +189 -0
- package/bin/dave.js +436 -184
- package/docs/bin-dave.md +85 -35
- package/docs/cdn-ssh.md +100 -0
- package/docs/creating-agents.md +301 -0
- package/docs/creating-toolsets.md +336 -0
- package/docs/docs-organization.md +48 -0
- package/docs/project-overview.md +86 -51
- package/lib/API/elevenlabs.io/music.compose.md +441 -0
- package/lib/API/elevenlabs.io/music.create-composition-plan.md +370 -0
- package/lib/API/elevenlabs.io/music.stream.md +425 -0
- package/lib/API/lalal.ai/lalal.js +445 -0
- package/lib/API/lalal.ai/openapi.json +2614 -0
- package/lib/API/minimax/ImageToolset.js +82 -37
- package/lib/API/minimax/MusicToolset.js +125 -79
- package/lib/API/minimax/VideoToolset.js +170 -167
- package/lib/API/minimax/image.js +5 -1
- package/lib/API/minimax/music.js +210 -23
- package/lib/API/minimax/video.js +242 -53
- package/lib/API/mureka/MusicToolset.js +646 -0
- package/lib/API/mureka/README.md +41 -0
- package/lib/API/mureka/index.js +7 -0
- package/lib/API/mureka/music.js +658 -0
- package/lib/API/openai.com/index.js +7 -0
- package/lib/API/openai.com/{reponses/text.js → responses.js} +64 -18
- package/lib/API/openai.com/video.create.character.md +40 -0
- package/lib/API/openai.com/video.create.md +219 -0
- package/lib/API/openai.com/video.delete.md +44 -0
- package/lib/API/openai.com/video.download.md +31 -0
- package/lib/API/openai.com/video.edit.md +155 -0
- package/lib/API/openai.com/video.extend.md +166 -0
- package/lib/API/openai.com/video.fetch.character.md +43 -0
- package/lib/API/openai.com/video.js +784 -0
- package/lib/API/openai.com/video.list.md +201 -0
- package/lib/API/openai.com/video.remix.md +175 -0
- package/lib/API/openai.com/video.retrieve.md +139 -0
- package/lib/API/openai.com/videoToolset.js +616 -0
- package/lib/API/stability.ai/ImageToolset.js +131 -40
- package/lib/API/stability.ai/MusicToolset.js +79 -47
- package/lib/API/stability.ai/audio.js +63 -131
- package/lib/API/x.ai/chat.responses.md +1040 -0
- package/lib/API/x.ai/image.js +229 -59
- package/lib/API/x.ai/imageToolset.js +376 -0
- package/lib/API/x.ai/index.js +1 -1
- package/lib/API/x.ai/responses.js +9 -18
- package/lib/Agent.js +271 -0
- package/lib/Agent.js.old +284 -0
- package/lib/AgentLauncher.js +562 -0
- package/lib/Cli.js +87 -13
- package/lib/Prompt.js +23 -1
- package/lib/Session.js +5 -4
- package/lib/ToolSet.js +102 -6
- package/lib/agentLoader.js +369 -0
- package/lib/cdn.js +67 -231
- package/lib/{CdnToolset.js → cdnToolset.js} +47 -64
- package/lib/defaultToolsets.js +43 -0
- package/lib/fafs.js +1 -1
- package/lib/genericToolset.js +442 -119
- package/lib/handOffToolset.js +179 -0
- package/lib/index.js +34 -27
- package/lib/toolsetLoader.js +248 -0
- package/package.json +11 -5
- package/types/API/lalal.ai/lalal.d.ts +116 -0
- package/types/API/minimax/image.d.ts +2 -1
- package/types/API/minimax/music.d.ts +189 -26
- package/types/API/minimax/video.d.ts +100 -31
- package/types/API/mureka/index.d.ts +7 -0
- package/types/API/mureka/music.d.ts +472 -0
- package/types/API/openai.com/index.d.ts +7 -0
- package/types/API/openai.com/{reponses/text.d.ts → responses.d.ts} +11 -11
- package/types/API/openai.com/video.d.ts +409 -0
- package/types/API/openai.com/videoToolset.d.ts +24 -0
- package/types/API/stability.ai/audio.d.ts +14 -103
- package/types/API/stability.ai/image.d.ts +2 -2
- package/types/API/x.ai/image.d.ts +138 -26
- package/types/API/x.ai/imageToolset.d.ts +3 -0
- package/types/API/x.ai/index.d.ts +1 -1
- package/types/API/x.ai/responses.d.ts +4 -4
- package/types/Agent.d.ts +123 -0
- package/types/AgentLauncher.d.ts +222 -0
- package/types/Cli.d.ts +28 -8
- package/types/Prompt.d.ts +23 -5
- package/types/Session.d.ts +1 -1
- package/types/ToolSet.d.ts +10 -0
- package/types/agentLoader.d.ts +78 -0
- package/types/cdn.d.ts +15 -90
- package/types/defaultToolsets.d.ts +9 -0
- package/types/fafs.d.ts +1 -1
- package/types/genericToolset.d.ts +1 -1
- package/types/handOffToolset.d.ts +28 -0
- package/types/index.d.ts +19 -17
- package/types/toolsetLoader.d.ts +114 -0
- package/utils/format_log.js +101 -23
- package/utils/launch_agent.js +18 -0
- package/utils/list_sessions.sh +13 -5
- package/utils/search_sessions.sh +65 -29
- package/utils/toolsets.js +33 -0
- package/README.md.bak.1779452127 +0 -240
- package/agents/codeserver.sh +0 -47
- package/agents/daisy_agent.js +0 -173
- package/agents/docs_agent.js +0 -148
- package/agents/memory_agent.js +0 -263
- package/agents/minimax.js +0 -173
- package/agents/npm_agent.js +0 -202
- package/agents/prompt_agent.js +0 -133
- package/agents/readme_agent.js +0 -148
- package/agents/spawn_agent.js +0 -160
- package/agents/stability.js +0 -173
- package/agents/todo_agent.js +0 -175
- package/bin/codeDave +0 -58
- package/docs/agent-dave-websocket-protocol.md +0 -180
- package/docs/agent-manager.md +0 -244
- package/docs/codeserver-pattern.md +0 -191
- package/docs/generic-toolset.md +0 -326
- package/docs/howtos/agent-networking.md +0 -253
- package/docs/howtos/spawn-agents.md.bak +0 -200
- package/docs/howtos/spawn-agents.md.bak_new +0 -200
- package/docs/multi-agent-clusters.md +0 -265
- package/docs/music-toolsets.md +0 -137
- package/docs/path-resolution-best-practices.md +0 -104
- package/docs/plans/minimax-music-generation.md +0 -80
- package/docs/plans/unified-agent-architecture.md +0 -146
- package/docs/plans/websocket-streaming-plan.md.bak +0 -317
- package/docs/prompt/spawn_agent.md +0 -175
- package/docs/prompt/spawn_agent.md.bak +0 -201
- package/docs/prompt/task_clarification_and_documentation.md +0 -35
- package/docs/prompt-class.md +0 -141
- package/docs/todo-archive-infra-2026-04-21.md +0 -15
- package/docs/todo-archive-v0.0.8.md +0 -1
- package/docs/todo-archive-v0.1.0.md +0 -32
- package/docs/todo-archive.md +0 -44
- package/docs/tools-syntax-validation.md +0 -121
- package/docs/toolset.md +0 -164
- package/docs/xai-responses.md +0 -111
- package/docs/xai_collections.md +0 -106
- package/lib/API/x.ai/ImageToolset.js +0 -165
- package/lib/API/x.ai/text.js +0 -415
- package/lib/AgentClient.js +0 -248
- package/lib/AgentManager.js +0 -245
- package/lib/AgentServer.js +0 -404
- package/lib/wsCli.js +0 -287
- package/lib/wsIO.js +0 -90
- package/types/API/x.ai/text.d.ts +0 -286
- package/types/AgentClient.d.ts +0 -109
- package/types/AgentManager.d.ts +0 -100
- package/types/AgentServer.d.ts +0 -89
- package/types/wsCli.d.ts +0 -17
- package/types/wsIO.d.ts +0 -30
- package/utils/test.sh +0 -46
- /package/docs/{suggestions.md → _notes/token-counts.md} +0 -0
- /package/lib/API/openai.com/{reponses/MESSAGES.md → MESSAGES.md} +0 -0
- /package/types/API/{x.ai/ImageToolset.d.ts → mureka/MusicToolset.d.ts} +0 -0
- /package/types/{CdnToolset.d.ts → cdnToolset.d.ts} +0 -0
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @file lib/API/openai.com/video.js
|
|
6
|
+
* @module openai.com/video
|
|
7
|
+
* @description Thin HTTP wrapper around OpenAI video endpoints.
|
|
8
|
+
*
|
|
9
|
+
* Video job calls submit a request and then poll until the video reaches a
|
|
10
|
+
* terminal state. Successful jobs can optionally download the generated asset
|
|
11
|
+
* to `.cache/openai/`.
|
|
12
|
+
*
|
|
13
|
+
* Documentation source files:
|
|
14
|
+
* - video.create.md
|
|
15
|
+
* - video.retrieve.md
|
|
16
|
+
* - video.download.md
|
|
17
|
+
* - video.edit.md
|
|
18
|
+
* - video.extend.md
|
|
19
|
+
* - video.remix.md
|
|
20
|
+
* - video.list.md
|
|
21
|
+
* - video.delete.md
|
|
22
|
+
* - video.create.character.md
|
|
23
|
+
* - video.fetch.character.md
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const API_URL = 'https://api.openai.com/v1';
|
|
27
|
+
const TMP_DIR = path.join(process.cwd(), '.cache', 'openai');
|
|
28
|
+
|
|
29
|
+
/** Terminal video statuses returned by OpenAI. */
|
|
30
|
+
const TERMINAL_STATUSES = new Set(['completed', 'failed']);
|
|
31
|
+
|
|
32
|
+
/** Non-terminal video statuses returned by OpenAI. */
|
|
33
|
+
const PENDING_STATUSES = new Set(['queued', 'in_progress']);
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* JSON-compatible primitive value.
|
|
38
|
+
* @typedef {string|number|boolean|null} JsonPrimitive
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* JSON-compatible value accepted in OpenAI JSON request bodies.
|
|
43
|
+
* @typedef {JsonPrimitive|JsonObject|JsonArray} JsonValue
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* JSON-compatible object accepted in OpenAI JSON request bodies.
|
|
48
|
+
* @typedef {{[key: string]: JsonValue|undefined}} JsonObject
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* JSON-compatible array accepted in OpenAI JSON request bodies.
|
|
53
|
+
* @typedef {Array<JsonValue|undefined>} JsonArray
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* HTTP header map used for OpenAI requests.
|
|
58
|
+
* @typedef {Object} OpenAIHeaders
|
|
59
|
+
* @property {string} ['User-Agent'] - User-Agent header value.
|
|
60
|
+
* @property {string} ['Content-Type'] - Content type for JSON requests. Omitted for multipart and raw downloads.
|
|
61
|
+
* @property {string} Authorization - Bearer token authorization header.
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Common wrapper request options.
|
|
66
|
+
* @typedef {Object} RequestOptions
|
|
67
|
+
* @property {string} [userAgent='@j-o-r/agents'] - User-Agent header value.
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Query parameters serialized onto an OpenAI request URL.
|
|
72
|
+
* Undefined and null values are skipped.
|
|
73
|
+
* @typedef {{[key: string]: string|number|boolean|null|undefined}} QueryParams
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Parsed response body returned by fetch helpers.
|
|
78
|
+
* @typedef {JsonValue|string|ArrayBuffer|Blob|undefined} ParsedResponseBody
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Downloadable video content variant.
|
|
83
|
+
* @typedef {'video'|'thumbnail'|'spritesheet'} VideoAssetVariant
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Supported OpenAI video model identifier.
|
|
88
|
+
* @typedef {'sora-2'|'sora-2-pro'|'sora-2-2025-10-06'|'sora-2-pro-2025-10-06'|'sora-2-2025-12-08'|string} VideoModel
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Supported base clip duration for video creation.
|
|
93
|
+
* @typedef {'4'|'8'|'12'|4|8|12|number} VideoSeconds
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Supported generated segment duration for video extensions.
|
|
98
|
+
* @typedef {'4'|'8'|'12'|'16'|'20'|4|8|12|16|20|number} VideoExtensionSeconds
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Supported generated video output resolution.
|
|
103
|
+
* @typedef {'720x1280'|'1280x720'|'1024x1792'|'1792x1024'} VideoSize
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Lifecycle status of an OpenAI video job.
|
|
108
|
+
* @typedef {'queued'|'in_progress'|'completed'|'failed'|string} VideoStatus
|
|
109
|
+
*/
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Error payload returned for a failed video generation job.
|
|
113
|
+
* @typedef {Object} VideoCreateError
|
|
114
|
+
* @property {string} code - Machine-readable error code.
|
|
115
|
+
* @property {string} message - Human-readable error description.
|
|
116
|
+
*/
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Optional image reference object used to guide video creation.
|
|
120
|
+
* Exactly one of `image_url` or `file_id` should be provided.
|
|
121
|
+
* @typedef {Object} VideoInputReference
|
|
122
|
+
* @property {string} [image_url] - Fully-qualified image URL or base64 data URL.
|
|
123
|
+
* @property {string} [file_id] - OpenAI file identifier for an uploaded image reference.
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Reference to an existing generated video.
|
|
128
|
+
* @typedef {Object} VideoReference
|
|
129
|
+
* @property {string} id - Identifier of the completed source video.
|
|
130
|
+
*/
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Structured metadata for an OpenAI video generation job.
|
|
134
|
+
* @typedef {Object} VideoObject
|
|
135
|
+
* @property {string} id - Unique identifier for the video job.
|
|
136
|
+
* @property {number|null} [completed_at] - Unix timestamp in seconds for completion, when finished.
|
|
137
|
+
* @property {number} [created_at] - Unix timestamp in seconds when the job was created.
|
|
138
|
+
* @property {VideoCreateError|null} [error] - Error payload when generation failed.
|
|
139
|
+
* @property {number|null} [expires_at] - Unix timestamp in seconds when downloadable assets expire.
|
|
140
|
+
* @property {VideoModel} [model] - Video generation model that produced the job.
|
|
141
|
+
* @property {'video'} [object] - Object type marker for video job responses.
|
|
142
|
+
* @property {number} [progress] - Approximate completion percentage from 0 to 100.
|
|
143
|
+
* @property {string} [prompt] - Prompt used to generate, edit, extend, or remix the video.
|
|
144
|
+
* @property {string|null} [remixed_from_video_id] - Source video id when this video is a remix.
|
|
145
|
+
* @property {string} [seconds] - Duration of the generated clip, or stitched total duration for extensions.
|
|
146
|
+
* @property {VideoSize} [size] - Output resolution.
|
|
147
|
+
* @property {VideoStatus} status - Current lifecycle status of the video job.
|
|
148
|
+
* @property {VideoDownloadResult} [content] - Downloaded media data when a wrapper is called with download enabled.
|
|
149
|
+
*/
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Response returned after deleting a video.
|
|
153
|
+
* @typedef {Object} VideoDeleteResponse
|
|
154
|
+
* @property {string} id - Identifier of the deleted video.
|
|
155
|
+
* @property {boolean} deleted - Whether the resource was deleted.
|
|
156
|
+
* @property {'video.deleted'} object - Object type marker for delete responses.
|
|
157
|
+
*/
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Query parameters for listing generated videos.
|
|
161
|
+
* @typedef {Object} VideoListQuery
|
|
162
|
+
* @property {string} [after] - Pagination cursor from the previous response.
|
|
163
|
+
* @property {number} [limit] - Maximum number of videos to retrieve.
|
|
164
|
+
* @property {'asc'|'desc'} [order] - Sort order by timestamp.
|
|
165
|
+
*/
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Paginated list response for generated videos.
|
|
169
|
+
* @typedef {Object} VideoListResponse
|
|
170
|
+
* @property {VideoObject[]} data - Page of video jobs.
|
|
171
|
+
* @property {string} [first_id] - Identifier of the first item in the page.
|
|
172
|
+
* @property {boolean} [has_more] - Whether more items are available.
|
|
173
|
+
* @property {string} [last_id] - Identifier of the last item in the page.
|
|
174
|
+
* @property {'list'} object - Object type marker for list responses.
|
|
175
|
+
*/
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Response returned for character creation and retrieval.
|
|
179
|
+
* @typedef {Object} VideoCharacter
|
|
180
|
+
* @property {string} id - Identifier for the character cameo.
|
|
181
|
+
* @property {number} created_at - Unix timestamp in seconds when the character was created.
|
|
182
|
+
* @property {string} name - Display name for the character.
|
|
183
|
+
*/
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Result returned by authenticated media download helpers.
|
|
187
|
+
* @typedef {Object} VideoDownloadResult
|
|
188
|
+
* @property {Buffer} buffer - Downloaded media bytes.
|
|
189
|
+
* @property {string} contentType - Response content type.
|
|
190
|
+
* @property {number} bytes - Number of downloaded bytes.
|
|
191
|
+
* @property {string} [local_path] - Absolute local path when media was saved to disk.
|
|
192
|
+
*/
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Options for polling a video job until it reaches a terminal state.
|
|
196
|
+
* @typedef {RequestOptions & Object} PollOptions
|
|
197
|
+
* @property {number} [maxWaitMs=600000] - Maximum total polling time in milliseconds.
|
|
198
|
+
* @property {number} [pollIntervalMs=5000] - Delay between poll requests in milliseconds.
|
|
199
|
+
*/
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Options for submitting a create-video request.
|
|
203
|
+
* @typedef {RequestOptions & PollOptions & Object} SubmitVideoOptions
|
|
204
|
+
* @property {VideoModel} [model='sora-2'] - Video generation model.
|
|
205
|
+
* @property {VideoSeconds} [seconds='4'] - Clip duration in seconds.
|
|
206
|
+
* @property {VideoSize} [size='720x1280'] - Output resolution.
|
|
207
|
+
* @property {string|VideoInputReference} [input_reference] - Optional image reference URL/data URL/file reference.
|
|
208
|
+
* @property {JsonObject} [extra] - Additional request fields merged into the JSON body.
|
|
209
|
+
*/
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Options for creating a video and optionally downloading the completed result.
|
|
213
|
+
* @typedef {SubmitVideoOptions & Object} CreateVideoOptions
|
|
214
|
+
* @property {boolean} [download=false] - Download final video after completion.
|
|
215
|
+
* @property {VideoAssetVariant} [variant='video'] - Asset variant to download when download is enabled.
|
|
216
|
+
* @property {string} [filenamePrefix='openai-video'] - Local filename prefix used when saving downloaded media.
|
|
217
|
+
*/
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Options for emergency polling after a previous create/edit/extend/remix timeout.
|
|
221
|
+
* @typedef {PollOptions & Object} EmergencyPollVideoOptions
|
|
222
|
+
* @property {number} [maxWaitMs=3600000] - Maximum total polling time in milliseconds.
|
|
223
|
+
* @property {number} [pollIntervalMs=30000] - Delay between poll requests in milliseconds.
|
|
224
|
+
* @property {boolean} [download=false] - Download final media when completed.
|
|
225
|
+
* @property {VideoAssetVariant} [variant='video'] - Asset variant to download.
|
|
226
|
+
* @property {string} [filenamePrefix='openai-video'] - Local filename prefix used when saving downloaded media.
|
|
227
|
+
*/
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Options for downloading generated video media or preview assets.
|
|
231
|
+
* @typedef {RequestOptions & Object} DownloadVideoOptions
|
|
232
|
+
* @property {VideoAssetVariant} [variant='video'] - Asset variant to download.
|
|
233
|
+
* @property {boolean} [save=false] - Save bytes to `.cache/openai/`.
|
|
234
|
+
* @property {string} [filenamePrefix='openai-video'] - Local filename prefix.
|
|
235
|
+
*/
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Options for edit, extension, and remix requests.
|
|
239
|
+
* @typedef {RequestOptions & PollOptions & Object} VideoMutationOptions
|
|
240
|
+
* @property {JsonObject} [extra] - Additional request fields merged into the JSON body.
|
|
241
|
+
*/
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Local or in-memory video input accepted by createCharacter().
|
|
245
|
+
* @typedef {string|Buffer|Blob|ArrayBuffer} CharacterVideoInput
|
|
246
|
+
*/
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Blob conversion result used for multipart character upload.
|
|
250
|
+
* @typedef {Object} VideoBlobResult
|
|
251
|
+
* @property {Blob} blob - Blob ready to append to FormData.
|
|
252
|
+
* @property {string} filename - Filename sent with the multipart upload.
|
|
253
|
+
*/
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Get the default JSON headers.
|
|
257
|
+
* @param {string} [USER_AGENT='@j-o-r/agents'] - User-Agent header value.
|
|
258
|
+
* @returns {OpenAIHeaders} Headers object.
|
|
259
|
+
* @throws {Error} If OPENAIKEY is missing.
|
|
260
|
+
*/
|
|
261
|
+
const getHeaders = (USER_AGENT = '@j-o-r/agents') => {
|
|
262
|
+
if (!process.env['OPENAIKEY']) {
|
|
263
|
+
throw new Error('Missing OPENAIKEY!export OPENAIKEY=<OPENAI_API_KEY>');
|
|
264
|
+
}
|
|
265
|
+
const KEY = process.env['OPENAIKEY'];
|
|
266
|
+
return {
|
|
267
|
+
'User-Agent': USER_AGENT,
|
|
268
|
+
'Content-Type': 'application/json',
|
|
269
|
+
'Authorization': `Bearer ${KEY}`
|
|
270
|
+
};
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Get auth headers for multipart/form-data requests.
|
|
275
|
+
* Do not set Content-Type; fetch/FormData must set the boundary.
|
|
276
|
+
* @param {string} [USER_AGENT='@j-o-r/agents'] - User-Agent header value.
|
|
277
|
+
* @returns {OpenAIHeaders} Headers object.
|
|
278
|
+
* @throws {Error} If OPENAIKEY is missing.
|
|
279
|
+
*/
|
|
280
|
+
const getMultipartHeaders = (USER_AGENT = '@j-o-r/agents') => {
|
|
281
|
+
const headers = getHeaders(USER_AGENT);
|
|
282
|
+
delete headers['Content-Type'];
|
|
283
|
+
return headers;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Wait for a number of milliseconds.
|
|
288
|
+
* @param {number} ms - Milliseconds to sleep.
|
|
289
|
+
* @returns {Promise<void>}
|
|
290
|
+
*/
|
|
291
|
+
function sleep(ms) {
|
|
292
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Ensure the local cache directory exists.
|
|
297
|
+
* @returns {Promise<void>}
|
|
298
|
+
*/
|
|
299
|
+
async function ensureTmpDir() {
|
|
300
|
+
await fs.mkdir(TMP_DIR, { recursive: true });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Convert a plain object to a URL query string.
|
|
305
|
+
* Undefined/null values are skipped.
|
|
306
|
+
* @param {QueryParams} [query={}] - Query parameters.
|
|
307
|
+
* @returns {string} Query string, including leading `?` when non-empty.
|
|
308
|
+
*/
|
|
309
|
+
function toQueryString(query = {}) {
|
|
310
|
+
const params = new URLSearchParams();
|
|
311
|
+
for (const [key, value] of Object.entries(query)) {
|
|
312
|
+
if (value === undefined || value === null) continue;
|
|
313
|
+
params.set(key, String(value));
|
|
314
|
+
}
|
|
315
|
+
const text = params.toString();
|
|
316
|
+
return text ? `?${text}` : '';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Parse a fetch Response as JSON when possible, otherwise text.
|
|
321
|
+
* @param {Response} response - Fetch response.
|
|
322
|
+
* @returns {Promise<ParsedResponseBody>} Parsed body.
|
|
323
|
+
*/
|
|
324
|
+
async function parseResponseBody(response) {
|
|
325
|
+
const contentType = response.headers.get('content-type') || '';
|
|
326
|
+
if (contentType.includes('application/json')) return response.json();
|
|
327
|
+
const text = await response.text();
|
|
328
|
+
try {
|
|
329
|
+
return JSON.parse(text);
|
|
330
|
+
} catch {
|
|
331
|
+
return text;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Make a JSON API request to OpenAI.
|
|
337
|
+
* @param {string} endpoint - Endpoint path, e.g. `/videos`.
|
|
338
|
+
* @param {'GET'|'POST'|'DELETE'} [method='GET'] - HTTP method.
|
|
339
|
+
* @param {JsonObject|null} [body=null] - JSON body.
|
|
340
|
+
* @param {QueryParams} [query={}] - Query parameters.
|
|
341
|
+
* @param {RequestOptions} [options={}] - Request options.
|
|
342
|
+
* @param {string} [options.userAgent='@j-o-r/agents'] - User-Agent header value.
|
|
343
|
+
* @returns {Promise<ParsedResponseBody>} Parsed API response.
|
|
344
|
+
* @throws {Error} On non-2xx responses.
|
|
345
|
+
*/
|
|
346
|
+
async function apiRequest(endpoint, method = 'GET', body = null, query = {}, options = {}) {
|
|
347
|
+
const url = `${API_URL}${endpoint}${toQueryString(query)}`;
|
|
348
|
+
const response = await fetch(url, {
|
|
349
|
+
method,
|
|
350
|
+
headers: getHeaders(options.userAgent),
|
|
351
|
+
body: body === null || body === undefined ? undefined : JSON.stringify(body)
|
|
352
|
+
});
|
|
353
|
+
const parsed = await parseResponseBody(response);
|
|
354
|
+
|
|
355
|
+
if (!response.ok) {
|
|
356
|
+
throw new Error(`OpenAI video API error ${response.status}: ${JSON.stringify(parsed)}`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return parsed;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Download bytes from an authenticated OpenAI endpoint.
|
|
364
|
+
* @param {string} endpoint - Endpoint path.
|
|
365
|
+
* @param {QueryParams} [query={}] - Query parameters.
|
|
366
|
+
* @param {RequestOptions} [options={}] - Request options.
|
|
367
|
+
* @param {string} [options.userAgent='@j-o-r/agents'] - User-Agent header value.
|
|
368
|
+
* @returns {Promise<VideoDownloadResult>}
|
|
369
|
+
* @throws {Error} On non-2xx responses.
|
|
370
|
+
*/
|
|
371
|
+
async function apiDownload(endpoint, query = {}, options = {}) {
|
|
372
|
+
const url = `${API_URL}${endpoint}${toQueryString(query)}`;
|
|
373
|
+
const headers = getHeaders(options.userAgent);
|
|
374
|
+
delete headers['Content-Type'];
|
|
375
|
+
|
|
376
|
+
const response = await fetch(url, { method: 'GET', headers });
|
|
377
|
+
if (!response.ok) {
|
|
378
|
+
const parsed = await parseResponseBody(response);
|
|
379
|
+
throw new Error(`OpenAI video download error ${response.status}: ${JSON.stringify(parsed)}`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
383
|
+
return {
|
|
384
|
+
buffer,
|
|
385
|
+
contentType: response.headers.get('content-type') || 'application/octet-stream',
|
|
386
|
+
bytes: buffer.length
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Return a useful file extension for a content variant / content type.
|
|
392
|
+
* @param {VideoAssetVariant} variant - Asset variant.
|
|
393
|
+
* @param {string} contentType - Response content type.
|
|
394
|
+
* @returns {string} File extension without a dot.
|
|
395
|
+
*/
|
|
396
|
+
function getVariantExtension(variant = 'video', contentType = '') {
|
|
397
|
+
if (variant === 'thumbnail') return contentType.includes('png') ? 'png' : 'jpg';
|
|
398
|
+
if (variant === 'spritesheet') return contentType.includes('png') ? 'png' : 'jpg';
|
|
399
|
+
return 'mp4';
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Save downloaded media bytes to `.cache/openai/`.
|
|
404
|
+
* @param {Buffer} buffer - Media bytes.
|
|
405
|
+
* @param {string} [filenamePrefix='openai-video'] - Filename prefix.
|
|
406
|
+
* @param {string} [ext='mp4'] - File extension.
|
|
407
|
+
* @returns {Promise<string>} Absolute local path.
|
|
408
|
+
*/
|
|
409
|
+
async function saveMediaToLocal(buffer, filenamePrefix = 'openai-video', ext = 'mp4') {
|
|
410
|
+
await ensureTmpDir();
|
|
411
|
+
const filename = `${filenamePrefix}-${Date.now()}.${ext}`;
|
|
412
|
+
const localPath = path.join(TMP_DIR, filename);
|
|
413
|
+
await fs.writeFile(localPath, buffer);
|
|
414
|
+
return localPath;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Normalize an image reference for video creation.
|
|
419
|
+
* @param {string|VideoInputReference} inputReference - URL/data URL/file reference.
|
|
420
|
+
* @returns {VideoInputReference|undefined} OpenAI input_reference object.
|
|
421
|
+
*/
|
|
422
|
+
function normalizeInputReference(inputReference) {
|
|
423
|
+
if (!inputReference) return undefined;
|
|
424
|
+
if (typeof inputReference === 'string') return { image_url: inputReference };
|
|
425
|
+
return inputReference;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Validate a completed/failed video response and throw on failed jobs.
|
|
430
|
+
* @param {VideoObject} video - Video object.
|
|
431
|
+
* @returns {VideoObject} Video object.
|
|
432
|
+
* @throws {Error} If the video job failed.
|
|
433
|
+
*/
|
|
434
|
+
function assertTerminalVideo(video) {
|
|
435
|
+
if (video?.status === 'failed') {
|
|
436
|
+
throw new Error(`OpenAI video generation failed: ${JSON.stringify(video.error || video)}`);
|
|
437
|
+
}
|
|
438
|
+
return video;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Poll a video until it reaches a terminal state.
|
|
443
|
+
*
|
|
444
|
+
* This is the normal polling helper used by create/edit/extend/remix wrappers.
|
|
445
|
+
* Use `emergencyPollVideo()` for edge cases after a caller-side timeout.
|
|
446
|
+
*
|
|
447
|
+
* @param {string} videoId - OpenAI video id.
|
|
448
|
+
* @param {PollOptions} [options={}] - Polling options.
|
|
449
|
+
* @param {number} [options.maxWaitMs=600000] - Maximum wait time in ms.
|
|
450
|
+
* @param {number} [options.pollIntervalMs=5000] - Delay between polls in ms.
|
|
451
|
+
* @param {string} [options.userAgent='@j-o-r/agents'] - User-Agent header value.
|
|
452
|
+
* @returns {Promise<VideoObject>} Completed video object.
|
|
453
|
+
* @throws {Error} On failed job or timeout.
|
|
454
|
+
*/
|
|
455
|
+
async function pollVideo(videoId, options = {}) {
|
|
456
|
+
if (!videoId) throw new Error('pollVideo() requires a videoId');
|
|
457
|
+
|
|
458
|
+
const maxWaitMs = options.maxWaitMs ?? 600000;
|
|
459
|
+
const pollIntervalMs = options.pollIntervalMs ?? 5000;
|
|
460
|
+
const start = Date.now();
|
|
461
|
+
let lastVideo = null;
|
|
462
|
+
|
|
463
|
+
while (Date.now() - start <= maxWaitMs) {
|
|
464
|
+
lastVideo = await retrieveVideo(videoId, options);
|
|
465
|
+
const status = lastVideo?.status;
|
|
466
|
+
|
|
467
|
+
if (TERMINAL_STATUSES.has(status)) return assertTerminalVideo(lastVideo);
|
|
468
|
+
if (status && !PENDING_STATUSES.has(status)) return lastVideo;
|
|
469
|
+
|
|
470
|
+
await sleep(pollIntervalMs);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const timeout = new Error(
|
|
474
|
+
`OpenAI video polling timed out after ${maxWaitMs}ms for ${videoId}. ` +
|
|
475
|
+
`Use emergencyPollVideo('${videoId}') to continue polling. Last response: ${JSON.stringify(lastVideo)}`
|
|
476
|
+
);
|
|
477
|
+
timeout.videoId = videoId;
|
|
478
|
+
timeout.lastResponse = lastVideo;
|
|
479
|
+
throw timeout;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Emergency polling call for jobs that outlive a previous request timeout.
|
|
484
|
+
*
|
|
485
|
+
* Call this with the video id from a timed-out create/edit/extend/remix response
|
|
486
|
+
* or from the thrown timeout error's `videoId` property. Defaults are longer and
|
|
487
|
+
* less aggressive than normal polling.
|
|
488
|
+
*
|
|
489
|
+
* @param {string} videoId - OpenAI video id.
|
|
490
|
+
* @param {PollOptions} [options={}] - Polling options.
|
|
491
|
+
* @param {number} [options.maxWaitMs=3600000] - Maximum wait time in ms.
|
|
492
|
+
* @param {number} [options.pollIntervalMs=30000] - Delay between polls in ms.
|
|
493
|
+
* @param {boolean} [options.download=false] - Download final media when completed.
|
|
494
|
+
* @param {VideoAssetVariant} [options.variant='video'] - Asset variant to download.
|
|
495
|
+
* @param {string} [options.userAgent='@j-o-r/agents'] - User-Agent header value.
|
|
496
|
+
* @returns {Promise<VideoObject>} Completed video response, optionally with download data.
|
|
497
|
+
*/
|
|
498
|
+
async function emergencyPollVideo(videoId, options = {}) {
|
|
499
|
+
const video = await pollVideo(videoId, {
|
|
500
|
+
...options,
|
|
501
|
+
maxWaitMs: options.maxWaitMs ?? 3600000,
|
|
502
|
+
pollIntervalMs: options.pollIntervalMs ?? 30000
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
if (!options.download) return video;
|
|
506
|
+
|
|
507
|
+
const content = await downloadVideoContent(videoId, {
|
|
508
|
+
...options,
|
|
509
|
+
variant: options.variant ?? 'video',
|
|
510
|
+
save: true
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
return { ...video, content };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Submit a video create job without waiting.
|
|
518
|
+
* @param {string} prompt - Prompt describing the video.
|
|
519
|
+
* @param {SubmitVideoOptions} [options={}] - Create options.
|
|
520
|
+
* @param {VideoModel} [options.model='sora-2'] - Video model.
|
|
521
|
+
* @param {VideoSeconds} [options.seconds='4'] - Duration in seconds.
|
|
522
|
+
* @param {VideoSize} [options.size='720x1280'] - Output size.
|
|
523
|
+
* @param {string|VideoInputReference} [options.input_reference] - Optional image reference.
|
|
524
|
+
* @param {JsonObject} [options.extra] - Extra request fields.
|
|
525
|
+
* @returns {Promise<VideoObject>} Created video job response.
|
|
526
|
+
*/
|
|
527
|
+
async function submitVideo(prompt, options = {}) {
|
|
528
|
+
if (!prompt || typeof prompt !== 'string') throw new Error('submitVideo() requires a prompt string');
|
|
529
|
+
|
|
530
|
+
const body = {
|
|
531
|
+
model: options.model ?? 'sora-2',
|
|
532
|
+
prompt,
|
|
533
|
+
seconds: String(options.seconds ?? '4'),
|
|
534
|
+
size: options.size ?? '720x1280',
|
|
535
|
+
...options.extra
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
const inputReference = normalizeInputReference(options.input_reference);
|
|
539
|
+
if (inputReference) body.input_reference = inputReference;
|
|
540
|
+
|
|
541
|
+
return apiRequest('/videos', 'POST', body, {}, options);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Create a video and wait until the job completes.
|
|
546
|
+
* @param {string} prompt - Prompt describing the video.
|
|
547
|
+
* @param {CreateVideoOptions} [options={}] - Create and polling options.
|
|
548
|
+
* @param {boolean} [options.download=false] - Download final video after completion.
|
|
549
|
+
* @returns {Promise<VideoObject>} Completed video response, optionally with `content`.
|
|
550
|
+
*/
|
|
551
|
+
async function createVideo(prompt, options = {}) {
|
|
552
|
+
const created = await submitVideo(prompt, options);
|
|
553
|
+
const video = await pollVideo(created.id, options);
|
|
554
|
+
|
|
555
|
+
if (!options.download) return video;
|
|
556
|
+
|
|
557
|
+
const content = await downloadVideoContent(video.id, {
|
|
558
|
+
...options,
|
|
559
|
+
variant: options.variant ?? 'video',
|
|
560
|
+
save: true
|
|
561
|
+
});
|
|
562
|
+
return { ...video, content };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Retrieve the latest metadata for a generated video.
|
|
567
|
+
* @param {string} videoId - OpenAI video id.
|
|
568
|
+
* @param {RequestOptions} [options={}] - Request options.
|
|
569
|
+
* @returns {Promise<VideoObject>} Video object.
|
|
570
|
+
*/
|
|
571
|
+
async function retrieveVideo(videoId, options = {}) {
|
|
572
|
+
if (!videoId) throw new Error('retrieveVideo() requires a videoId');
|
|
573
|
+
return apiRequest(`/videos/${encodeURIComponent(videoId)}`, 'GET', null, {}, options);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* List recently generated videos for the current project.
|
|
578
|
+
* @param {VideoListQuery} [query={}] - List query parameters.
|
|
579
|
+
* @param {string} [query.after] - Pagination cursor.
|
|
580
|
+
* @param {number} [query.limit] - Number of items to retrieve.
|
|
581
|
+
* @param {'asc'|'desc'} [query.order] - Sort order.
|
|
582
|
+
* @param {RequestOptions} [options={}] - Request options.
|
|
583
|
+
* @returns {Promise<VideoListResponse>} List response.
|
|
584
|
+
*/
|
|
585
|
+
async function listVideos(query = {}, options = {}) {
|
|
586
|
+
return apiRequest('/videos', 'GET', null, query, options);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Delete a completed or failed video and its stored assets.
|
|
591
|
+
* @param {string} videoId - OpenAI video id.
|
|
592
|
+
* @param {RequestOptions} [options={}] - Request options.
|
|
593
|
+
* @returns {Promise<VideoDeleteResponse>} Delete response.
|
|
594
|
+
*/
|
|
595
|
+
async function deleteVideo(videoId, options = {}) {
|
|
596
|
+
if (!videoId) throw new Error('deleteVideo() requires a videoId');
|
|
597
|
+
return apiRequest(`/videos/${encodeURIComponent(videoId)}`, 'DELETE', null, {}, options);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Download generated video bytes or a preview asset.
|
|
602
|
+
* @param {string} videoId - OpenAI video id.
|
|
603
|
+
* @param {DownloadVideoOptions} [options={}] - Download options.
|
|
604
|
+
* @param {VideoAssetVariant} [options.variant='video'] - Asset variant.
|
|
605
|
+
* @param {boolean} [options.save=false] - Save bytes to `.cache/openai/`.
|
|
606
|
+
* @param {string} [options.filenamePrefix='openai-video'] - Local filename prefix.
|
|
607
|
+
* @returns {Promise<VideoDownloadResult>}
|
|
608
|
+
*/
|
|
609
|
+
async function downloadVideoContent(videoId, options = {}) {
|
|
610
|
+
if (!videoId) throw new Error('downloadVideoContent() requires a videoId');
|
|
611
|
+
|
|
612
|
+
const variant = options.variant ?? 'video';
|
|
613
|
+
const result = await apiDownload(`/videos/${encodeURIComponent(videoId)}/content`, { variant }, options);
|
|
614
|
+
|
|
615
|
+
if (options.save) {
|
|
616
|
+
const ext = getVariantExtension(variant, result.contentType);
|
|
617
|
+
result.local_path = await saveMediaToLocal(result.buffer, options.filenamePrefix ?? 'openai-video', ext);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return result;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Submit a video edit job without waiting.
|
|
625
|
+
* @param {string} videoId - Completed source video id.
|
|
626
|
+
* @param {string} prompt - Edit prompt.
|
|
627
|
+
* @param {VideoMutationOptions} [options={}] - Request options.
|
|
628
|
+
* @returns {Promise<VideoObject>} Created video job response.
|
|
629
|
+
*/
|
|
630
|
+
async function submitVideoEdit(videoId, prompt, options = {}) {
|
|
631
|
+
if (!videoId) throw new Error('submitVideoEdit() requires a videoId');
|
|
632
|
+
if (!prompt || typeof prompt !== 'string') throw new Error('submitVideoEdit() requires a prompt string');
|
|
633
|
+
|
|
634
|
+
return apiRequest('/videos/edits', 'POST', {
|
|
635
|
+
prompt,
|
|
636
|
+
video: { id: videoId },
|
|
637
|
+
...options.extra
|
|
638
|
+
}, {}, options);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Edit a video and wait until the job completes.
|
|
643
|
+
* @param {string} videoId - Completed source video id.
|
|
644
|
+
* @param {string} prompt - Edit prompt.
|
|
645
|
+
* @param {VideoMutationOptions} [options={}] - Request and polling options.
|
|
646
|
+
* @returns {Promise<VideoObject>} Completed video response.
|
|
647
|
+
*/
|
|
648
|
+
async function editVideo(videoId, prompt, options = {}) {
|
|
649
|
+
const created = await submitVideoEdit(videoId, prompt, options);
|
|
650
|
+
return pollVideo(created.id, options);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Submit a video extension job without waiting.
|
|
655
|
+
* @param {string} videoId - Completed source video id.
|
|
656
|
+
* @param {string} prompt - Extension prompt.
|
|
657
|
+
* @param {VideoExtensionSeconds} [seconds='4'] - New segment length.
|
|
658
|
+
* @param {VideoMutationOptions} [options={}] - Request options.
|
|
659
|
+
* @returns {Promise<VideoObject>} Created video job response.
|
|
660
|
+
*/
|
|
661
|
+
async function submitVideoExtension(videoId, prompt, seconds = '4', options = {}) {
|
|
662
|
+
if (!videoId) throw new Error('submitVideoExtension() requires a videoId');
|
|
663
|
+
if (!prompt || typeof prompt !== 'string') throw new Error('submitVideoExtension() requires a prompt string');
|
|
664
|
+
|
|
665
|
+
return apiRequest('/videos/extensions', 'POST', {
|
|
666
|
+
prompt,
|
|
667
|
+
seconds: String(seconds),
|
|
668
|
+
video: { id: videoId },
|
|
669
|
+
...options.extra
|
|
670
|
+
}, {}, options);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Extend a video and wait until the job completes.
|
|
675
|
+
* @param {string} videoId - Completed source video id.
|
|
676
|
+
* @param {string} prompt - Extension prompt.
|
|
677
|
+
* @param {VideoExtensionSeconds} [seconds='4'] - New segment length.
|
|
678
|
+
* @param {VideoMutationOptions} [options={}] - Request and polling options.
|
|
679
|
+
* @returns {Promise<VideoObject>} Completed video response.
|
|
680
|
+
*/
|
|
681
|
+
async function extendVideo(videoId, prompt, seconds = '4', options = {}) {
|
|
682
|
+
const created = await submitVideoExtension(videoId, prompt, seconds, options);
|
|
683
|
+
return pollVideo(created.id, options);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Submit a video remix job without waiting.
|
|
688
|
+
* @param {string} videoId - Completed source video id.
|
|
689
|
+
* @param {string} prompt - Remix prompt.
|
|
690
|
+
* @param {VideoMutationOptions} [options={}] - Request options.
|
|
691
|
+
* @returns {Promise<VideoObject>} Created video job response.
|
|
692
|
+
*/
|
|
693
|
+
async function submitVideoRemix(videoId, prompt, options = {}) {
|
|
694
|
+
if (!videoId) throw new Error('submitVideoRemix() requires a videoId');
|
|
695
|
+
if (!prompt || typeof prompt !== 'string') throw new Error('submitVideoRemix() requires a prompt string');
|
|
696
|
+
|
|
697
|
+
return apiRequest(`/videos/${encodeURIComponent(videoId)}/remix`, 'POST', {
|
|
698
|
+
prompt,
|
|
699
|
+
...options.extra
|
|
700
|
+
}, {}, options);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Remix a video and wait until the job completes.
|
|
705
|
+
* @param {string} videoId - Completed source video id.
|
|
706
|
+
* @param {string} prompt - Remix prompt.
|
|
707
|
+
* @param {VideoMutationOptions} [options={}] - Request and polling options.
|
|
708
|
+
* @returns {Promise<VideoObject>} Completed video response.
|
|
709
|
+
*/
|
|
710
|
+
async function remixVideo(videoId, prompt, options = {}) {
|
|
711
|
+
const created = await submitVideoRemix(videoId, prompt, options);
|
|
712
|
+
return pollVideo(created.id, options);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Create a Blob from a local file path, Buffer, Blob, or ArrayBuffer.
|
|
717
|
+
* @param {CharacterVideoInput} video - Video input.
|
|
718
|
+
* @returns {Promise<VideoBlobResult>} Blob and filename.
|
|
719
|
+
*/
|
|
720
|
+
async function toVideoBlob(video) {
|
|
721
|
+
if (typeof video === 'string') {
|
|
722
|
+
const buffer = await fs.readFile(video);
|
|
723
|
+
return { blob: new Blob([buffer], { type: 'video/mp4' }), filename: path.basename(video) };
|
|
724
|
+
}
|
|
725
|
+
if (Buffer.isBuffer(video)) return { blob: new Blob([video], { type: 'video/mp4' }), filename: 'video.mp4' };
|
|
726
|
+
if (video instanceof ArrayBuffer) return { blob: new Blob([video], { type: 'video/mp4' }), filename: 'video.mp4' };
|
|
727
|
+
if (video instanceof Blob) return { blob: video, filename: 'video.mp4' };
|
|
728
|
+
throw new Error('Unsupported video input for createCharacter()');
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Create a character from an uploaded video.
|
|
733
|
+
* @param {string} name - Display name for the character.
|
|
734
|
+
* @param {CharacterVideoInput} video - Local file path or video bytes.
|
|
735
|
+
* @param {RequestOptions} [options={}] - Request options.
|
|
736
|
+
* @returns {Promise<VideoCharacter>} Character response.
|
|
737
|
+
*/
|
|
738
|
+
async function createCharacter(name, video, options = {}) {
|
|
739
|
+
if (!name || typeof name !== 'string') throw new Error('createCharacter() requires a name string');
|
|
740
|
+
if (!video) throw new Error('createCharacter() requires a video input');
|
|
741
|
+
|
|
742
|
+
const { blob, filename } = await toVideoBlob(video);
|
|
743
|
+
const form = new FormData();
|
|
744
|
+
form.append('name', name);
|
|
745
|
+
form.append('video', blob, filename);
|
|
746
|
+
|
|
747
|
+
const response = await fetch(`${API_URL}/videos/characters`, {
|
|
748
|
+
method: 'POST',
|
|
749
|
+
headers: getMultipartHeaders(options.userAgent),
|
|
750
|
+
body: form
|
|
751
|
+
});
|
|
752
|
+
const parsed = await parseResponseBody(response);
|
|
753
|
+
|
|
754
|
+
if (!response.ok) {
|
|
755
|
+
throw new Error(`OpenAI character API error ${response.status}: ${JSON.stringify(parsed)}`);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return parsed;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Fetch a character by id.
|
|
763
|
+
* @param {string} characterId - Character id.
|
|
764
|
+
* @param {RequestOptions} [options={}] - Request options.
|
|
765
|
+
* @returns {Promise<VideoCharacter>} Character response.
|
|
766
|
+
*/
|
|
767
|
+
async function fetchCharacter(characterId, options = {}) {
|
|
768
|
+
if (!characterId) throw new Error('fetchCharacter() requires a characterId');
|
|
769
|
+
return apiRequest(`/videos/characters/${encodeURIComponent(characterId)}`, 'GET', null, {}, options);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export {
|
|
773
|
+
createVideo,
|
|
774
|
+
retrieveVideo,
|
|
775
|
+
listVideos,
|
|
776
|
+
deleteVideo,
|
|
777
|
+
downloadVideoContent,
|
|
778
|
+
emergencyPollVideo,
|
|
779
|
+
editVideo,
|
|
780
|
+
extendVideo,
|
|
781
|
+
remixVideo,
|
|
782
|
+
createCharacter,
|
|
783
|
+
fetchCharacter
|
|
784
|
+
};
|