@ai-sdk/prodia 1.0.22 → 1.0.23
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 +10 -0
- package/dist/index.d.mts +34 -2
- package/dist/index.d.ts +34 -2
- package/dist/index.js +676 -151
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +684 -139
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +4 -0
- package/src/prodia-api.ts +198 -0
- package/src/prodia-image-model.ts +16 -196
- package/src/prodia-language-model-settings.ts +6 -0
- package/src/prodia-language-model.ts +395 -0
- package/src/prodia-provider.ts +40 -8
- package/src/prodia-video-model-settings.ts +7 -0
- package/src/prodia-video-model.ts +282 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Experimental_VideoModelV3,
|
|
3
|
+
SharedV3Warning,
|
|
4
|
+
} from '@ai-sdk/provider';
|
|
5
|
+
import type { InferSchema } from '@ai-sdk/provider-utils';
|
|
6
|
+
import type { FetchFunction } from '@ai-sdk/provider-utils';
|
|
7
|
+
import {
|
|
8
|
+
combineHeaders,
|
|
9
|
+
convertBase64ToUint8Array,
|
|
10
|
+
lazySchema,
|
|
11
|
+
parseJSON,
|
|
12
|
+
parseProviderOptions,
|
|
13
|
+
postFormDataToApi,
|
|
14
|
+
postToApi,
|
|
15
|
+
resolve,
|
|
16
|
+
zodSchema,
|
|
17
|
+
} from '@ai-sdk/provider-utils';
|
|
18
|
+
import { z } from 'zod/v4';
|
|
19
|
+
import type { ProdiaModelConfig } from './prodia-api';
|
|
20
|
+
import {
|
|
21
|
+
buildProdiaProviderMetadata,
|
|
22
|
+
parseMultipart,
|
|
23
|
+
prodiaFailedResponseHandler,
|
|
24
|
+
prodiaJobResultSchema,
|
|
25
|
+
} from './prodia-api';
|
|
26
|
+
import type { ProdiaJobResult } from './prodia-api';
|
|
27
|
+
import type { ProdiaVideoModelId } from './prodia-video-model-settings';
|
|
28
|
+
|
|
29
|
+
export class ProdiaVideoModel implements Experimental_VideoModelV3 {
|
|
30
|
+
readonly specificationVersion = 'v3';
|
|
31
|
+
readonly maxVideosPerCall = 1;
|
|
32
|
+
|
|
33
|
+
get provider(): string {
|
|
34
|
+
return this.config.provider;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
constructor(
|
|
38
|
+
readonly modelId: ProdiaVideoModelId,
|
|
39
|
+
private readonly config: ProdiaModelConfig,
|
|
40
|
+
) {}
|
|
41
|
+
|
|
42
|
+
async doGenerate(
|
|
43
|
+
options: Parameters<Experimental_VideoModelV3['doGenerate']>[0],
|
|
44
|
+
): Promise<Awaited<ReturnType<Experimental_VideoModelV3['doGenerate']>>> {
|
|
45
|
+
const warnings: Array<SharedV3Warning> = [];
|
|
46
|
+
|
|
47
|
+
const prodiaOptions = await parseProviderOptions({
|
|
48
|
+
provider: 'prodia',
|
|
49
|
+
providerOptions: options.providerOptions,
|
|
50
|
+
schema: prodiaVideoModelOptionsSchema,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const jobConfig: Record<string, unknown> = {};
|
|
54
|
+
|
|
55
|
+
if (options.prompt !== undefined) {
|
|
56
|
+
jobConfig.prompt = options.prompt;
|
|
57
|
+
}
|
|
58
|
+
if (options.seed !== undefined) {
|
|
59
|
+
jobConfig.seed = options.seed;
|
|
60
|
+
}
|
|
61
|
+
if (prodiaOptions?.resolution !== undefined) {
|
|
62
|
+
jobConfig.resolution = prodiaOptions.resolution;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const body = {
|
|
66
|
+
type: this.modelId,
|
|
67
|
+
config: jobConfig,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const currentDate = this.config._internal?.currentDate?.() ?? new Date();
|
|
71
|
+
const combinedHeaders = combineHeaders(
|
|
72
|
+
await resolve(this.config.headers),
|
|
73
|
+
options.headers,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
let multipartResult: {
|
|
77
|
+
jobResult: ProdiaJobResult;
|
|
78
|
+
videoBytes: Uint8Array;
|
|
79
|
+
videoMediaType: string;
|
|
80
|
+
};
|
|
81
|
+
let responseHeaders: Record<string, string> | undefined;
|
|
82
|
+
|
|
83
|
+
if (options.image) {
|
|
84
|
+
// img2vid: multipart form-data request
|
|
85
|
+
const imageData = await resolveVideoFileData(
|
|
86
|
+
options.image,
|
|
87
|
+
this.config.fetch,
|
|
88
|
+
);
|
|
89
|
+
const formData = new FormData();
|
|
90
|
+
formData.append(
|
|
91
|
+
'job',
|
|
92
|
+
new Blob([JSON.stringify(body)], { type: 'application/json' }),
|
|
93
|
+
'job.json',
|
|
94
|
+
);
|
|
95
|
+
formData.append(
|
|
96
|
+
'input',
|
|
97
|
+
new Blob([imageData.bytes], { type: imageData.mediaType }),
|
|
98
|
+
'input' + getExtension(imageData.mediaType),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const result = await postFormDataToApi({
|
|
102
|
+
url: `${this.config.baseURL}/job?price=true`,
|
|
103
|
+
headers: {
|
|
104
|
+
...combinedHeaders,
|
|
105
|
+
Accept: 'multipart/form-data; video/mp4',
|
|
106
|
+
},
|
|
107
|
+
formData,
|
|
108
|
+
failedResponseHandler: prodiaFailedResponseHandler,
|
|
109
|
+
successfulResponseHandler: createVideoMultipartResponseHandler(),
|
|
110
|
+
abortSignal: options.abortSignal,
|
|
111
|
+
fetch: this.config.fetch,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
multipartResult = result.value;
|
|
115
|
+
responseHeaders = result.responseHeaders;
|
|
116
|
+
} else {
|
|
117
|
+
// txt2vid: JSON request
|
|
118
|
+
const result = await postToApi({
|
|
119
|
+
url: `${this.config.baseURL}/job?price=true`,
|
|
120
|
+
headers: {
|
|
121
|
+
...combinedHeaders,
|
|
122
|
+
Accept: 'multipart/form-data; video/mp4',
|
|
123
|
+
'Content-Type': 'application/json',
|
|
124
|
+
},
|
|
125
|
+
body: {
|
|
126
|
+
content: JSON.stringify(body),
|
|
127
|
+
values: body,
|
|
128
|
+
},
|
|
129
|
+
failedResponseHandler: prodiaFailedResponseHandler,
|
|
130
|
+
successfulResponseHandler: createVideoMultipartResponseHandler(),
|
|
131
|
+
abortSignal: options.abortSignal,
|
|
132
|
+
fetch: this.config.fetch,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
multipartResult = result.value;
|
|
136
|
+
responseHeaders = result.responseHeaders;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const { jobResult, videoBytes, videoMediaType } = multipartResult;
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
videos: [
|
|
143
|
+
{
|
|
144
|
+
type: 'binary',
|
|
145
|
+
data: videoBytes,
|
|
146
|
+
mediaType: videoMediaType,
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
warnings,
|
|
150
|
+
providerMetadata: {
|
|
151
|
+
prodia: {
|
|
152
|
+
videos: [buildProdiaProviderMetadata(jobResult)],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
response: {
|
|
156
|
+
modelId: this.modelId,
|
|
157
|
+
timestamp: currentDate,
|
|
158
|
+
headers: responseHeaders,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const prodiaVideoModelOptionsSchema = lazySchema(() =>
|
|
165
|
+
zodSchema(
|
|
166
|
+
z.object({
|
|
167
|
+
/**
|
|
168
|
+
* Video resolution (e.g. "480p", "720p").
|
|
169
|
+
*/
|
|
170
|
+
resolution: z.string().optional(),
|
|
171
|
+
}),
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
export type ProdiaVideoModelOptions = InferSchema<
|
|
176
|
+
typeof prodiaVideoModelOptionsSchema
|
|
177
|
+
>;
|
|
178
|
+
|
|
179
|
+
interface VideoMultipartResult {
|
|
180
|
+
jobResult: ProdiaJobResult;
|
|
181
|
+
videoBytes: Uint8Array;
|
|
182
|
+
videoMediaType: string;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function createVideoMultipartResponseHandler() {
|
|
186
|
+
return async ({
|
|
187
|
+
response,
|
|
188
|
+
}: {
|
|
189
|
+
response: Response;
|
|
190
|
+
}): Promise<{
|
|
191
|
+
value: VideoMultipartResult;
|
|
192
|
+
responseHeaders: Record<string, string>;
|
|
193
|
+
}> => {
|
|
194
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
195
|
+
const responseHeaders: Record<string, string> = {};
|
|
196
|
+
response.headers.forEach((value, key) => {
|
|
197
|
+
responseHeaders[key] = value;
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const boundaryMatch = contentType.match(/boundary=([^\s;]+)/);
|
|
201
|
+
if (!boundaryMatch) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
`Prodia response missing multipart boundary in content-type: ${contentType}`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const boundary = boundaryMatch[1];
|
|
207
|
+
|
|
208
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
209
|
+
const bytes = new Uint8Array(arrayBuffer);
|
|
210
|
+
|
|
211
|
+
const parts = parseMultipart(bytes, boundary);
|
|
212
|
+
|
|
213
|
+
let jobResult: ProdiaJobResult | undefined;
|
|
214
|
+
let videoBytes: Uint8Array | undefined;
|
|
215
|
+
let videoMediaType = 'video/mp4';
|
|
216
|
+
|
|
217
|
+
for (const part of parts) {
|
|
218
|
+
const contentDisposition = part.headers['content-disposition'] ?? '';
|
|
219
|
+
const partContentType = part.headers['content-type'] ?? '';
|
|
220
|
+
|
|
221
|
+
if (contentDisposition.includes('name="job"')) {
|
|
222
|
+
const jsonStr = new TextDecoder().decode(part.body);
|
|
223
|
+
jobResult = await parseJSON({
|
|
224
|
+
text: jsonStr,
|
|
225
|
+
schema: zodSchema(prodiaJobResultSchema),
|
|
226
|
+
});
|
|
227
|
+
} else if (contentDisposition.includes('name="output"')) {
|
|
228
|
+
videoBytes = part.body;
|
|
229
|
+
if (partContentType.startsWith('video/')) {
|
|
230
|
+
videoMediaType = partContentType;
|
|
231
|
+
}
|
|
232
|
+
} else if (partContentType.startsWith('video/')) {
|
|
233
|
+
videoBytes = part.body;
|
|
234
|
+
videoMediaType = partContentType;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!jobResult) {
|
|
239
|
+
throw new Error('Prodia multipart response missing job part');
|
|
240
|
+
}
|
|
241
|
+
if (!videoBytes) {
|
|
242
|
+
throw new Error('Prodia multipart response missing output video');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
value: { jobResult, videoBytes, videoMediaType },
|
|
247
|
+
responseHeaders,
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function resolveVideoFileData(
|
|
253
|
+
file: NonNullable<
|
|
254
|
+
Parameters<Experimental_VideoModelV3['doGenerate']>[0]['image']
|
|
255
|
+
>,
|
|
256
|
+
fetchFunction?: FetchFunction,
|
|
257
|
+
): Promise<{ bytes: Uint8Array; mediaType: string }> {
|
|
258
|
+
if (file.type === 'file') {
|
|
259
|
+
const data =
|
|
260
|
+
typeof file.data === 'string'
|
|
261
|
+
? convertBase64ToUint8Array(file.data)
|
|
262
|
+
: file.data;
|
|
263
|
+
return { bytes: data, mediaType: file.mediaType };
|
|
264
|
+
}
|
|
265
|
+
// URL type - fetch the data
|
|
266
|
+
const response = await (fetchFunction ?? globalThis.fetch)(file.url);
|
|
267
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
268
|
+
const mediaType =
|
|
269
|
+
response.headers.get('content-type') ?? 'application/octet-stream';
|
|
270
|
+
return { bytes: new Uint8Array(arrayBuffer), mediaType };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function getExtension(mediaType: string): string {
|
|
274
|
+
const map: Record<string, string> = {
|
|
275
|
+
'image/png': '.png',
|
|
276
|
+
'image/jpeg': '.jpg',
|
|
277
|
+
'image/webp': '.webp',
|
|
278
|
+
'video/mp4': '.mp4',
|
|
279
|
+
'video/webm': '.webm',
|
|
280
|
+
};
|
|
281
|
+
return map[mediaType] ?? '';
|
|
282
|
+
}
|