@agentmedia/schema 0.1.0

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.
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env tsx
2
+ // Copyright 2026 agent-media contributors. Apache-2.0 license.
3
+
4
+ /**
5
+ * Generates supabase/functions/_shared/schema.generated.ts
6
+ *
7
+ * Supabase edge functions run in Deno and cannot resolve Turborepo
8
+ * workspace packages. This script copies the const arrays from
9
+ * video.ts into a plain Deno-compatible file.
10
+ *
11
+ * The generated file is checked into git (Option A from spec §5.1).
12
+ * CI verifies it matches by re-running this script and diffing.
13
+ */
14
+
15
+ import {
16
+ DURATIONS,
17
+ TONES,
18
+ MUSIC_GENRES,
19
+ SUBTITLE_STYLES,
20
+ ASPECT_RATIOS,
21
+ SCENE_TYPES,
22
+ TEMPLATES,
23
+ VOICES,
24
+ TTS_PROVIDERS,
25
+ REVIEW_ANGLES,
26
+ PIP_POSITIONS,
27
+ PIP_SIZES,
28
+ PIP_ANIMATIONS,
29
+ PIP_FRAME_STYLES,
30
+ COMPOSITION_MODES,
31
+ BROLL_MODELS,
32
+ CREDITS_PER_SECOND,
33
+ SCRIPT_GENERATION_CREDIT_SURCHARGE,
34
+ } from '../src/video.js';
35
+
36
+ import { writeFileSync } from 'node:fs';
37
+ import { resolve, dirname } from 'node:path';
38
+ import { fileURLToPath } from 'node:url';
39
+
40
+ const __dirname = dirname(fileURLToPath(import.meta.url));
41
+ const outputPath = resolve(__dirname, '../../../supabase/functions/_shared/schema.generated.ts');
42
+
43
+ function toArrayLiteral(name: string, values: readonly (string | number)[]): string {
44
+ const formatted = values.map((v) => (typeof v === 'string' ? `"${v}"` : String(v)));
45
+ if (formatted.join(', ').length < 80) {
46
+ return `export const ${name} = [${formatted.join(', ')}] as const;`;
47
+ }
48
+ return `export const ${name} = [\n ${formatted.join(',\n ')},\n] as const;`;
49
+ }
50
+
51
+ const output = `// AUTO-GENERATED by packages/schema/scripts/generate-edge-schema.ts
52
+ // DO NOT EDIT. Source of truth: packages/schema/src/video.ts
53
+ // Re-generate: pnpm --filter @agent-media/schema generate:edge-schema
54
+
55
+ ${toArrayLiteral('DURATIONS', DURATIONS)}
56
+ ${toArrayLiteral('TONES', TONES)}
57
+ ${toArrayLiteral('MUSIC_GENRES', MUSIC_GENRES)}
58
+ ${toArrayLiteral('SUBTITLE_STYLES', SUBTITLE_STYLES)}
59
+ ${toArrayLiteral('ASPECT_RATIOS', ASPECT_RATIOS)}
60
+ ${toArrayLiteral('SCENE_TYPES', SCENE_TYPES)}
61
+ ${toArrayLiteral('TEMPLATES', TEMPLATES)}
62
+ ${toArrayLiteral('VOICES', VOICES)}
63
+ ${toArrayLiteral('TTS_PROVIDERS', TTS_PROVIDERS)}
64
+ ${toArrayLiteral('REVIEW_ANGLES', REVIEW_ANGLES)}
65
+ ${toArrayLiteral('PIP_POSITIONS', PIP_POSITIONS)}
66
+ ${toArrayLiteral('PIP_SIZES', PIP_SIZES)}
67
+ ${toArrayLiteral('PIP_ANIMATIONS', PIP_ANIMATIONS)}
68
+ ${toArrayLiteral('PIP_FRAME_STYLES', PIP_FRAME_STYLES)}
69
+ ${toArrayLiteral('COMPOSITION_MODES', COMPOSITION_MODES)}
70
+ ${toArrayLiteral('BROLL_MODELS', BROLL_MODELS)}
71
+
72
+ export const CREDITS_PER_SECOND = ${CREDITS_PER_SECOND};
73
+ export const SCRIPT_GENERATION_CREDIT_SURCHARGE = ${SCRIPT_GENERATION_CREDIT_SURCHARGE};
74
+ `;
75
+
76
+ writeFileSync(outputPath, output, 'utf-8');
77
+ console.log(`Generated: ${outputPath}`);
@@ -0,0 +1,403 @@
1
+ #!/usr/bin/env tsx
2
+ // Copyright 2026 agent-media contributors. Apache-2.0 license.
3
+
4
+ /**
5
+ * Generates generated/openapi.json from the GENERATORS registry.
6
+ * Addresses all findings from architect review 2026-04-11.
7
+ */
8
+
9
+ import { writeFileSync, mkdirSync } from 'node:fs';
10
+ import { resolve, dirname } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { zodToJsonSchema } from 'zod-to-json-schema';
13
+ import { GENERATORS } from '../src/generators.js';
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const outputDir = resolve(__dirname, '../../../generated');
17
+ const outputPath = resolve(outputDir, 'openapi.json');
18
+
19
+ // ── Field descriptions ──────────────────────────────────────────────────────
20
+
21
+ const FIELD_DESCRIPTIONS: Record<string, string> = {
22
+ script: 'Narration script for the video. Must be 50-3000 characters. Either script or prompt is required.',
23
+ prompt: 'AI generates a script from this description. 1-1000 characters. Either script or prompt is required.',
24
+ product_url: 'Product URL for context during AI script generation.',
25
+ actor_slug: 'AI actor slug for talking head. Run GET /v1/actors to browse. Example: "sofia", "marcus".',
26
+ persona_slug: 'Custom persona slug (cloned voice + face). Mutually exclusive with actor_slug.',
27
+ face_photo_url: 'Direct URL to a face photo. Mutually exclusive with actor_slug.',
28
+ voice: 'TTS voice ID. Auto-detected from face photo if omitted.',
29
+ target_duration: 'Target video duration in seconds. Must be 5, 10, or 15.',
30
+ style: 'Subtitle style. 17 options: hormozi, minimal, bold, karaoke, clean, tiktok, neon, fire, glow, pop, aesthetic, impact, pastel, electric, boxed, gradient, spotlight.',
31
+ tone: 'Voice tone for the narration.',
32
+ voice_speed: 'TTS speed multiplier. 1.0 is normal, 0.7 is slow, 1.5 is fast.',
33
+ music: 'Background music genre.',
34
+ cta: 'End screen call-to-action text. Max 100 characters. Example: "Follow for more".',
35
+ aspect_ratio: 'Video aspect ratio.',
36
+ template: 'Script structure template. Options: monologue, testimonial, product-review, problem-solution, saas-review, before-after, listicle, product-demo.',
37
+ composition_mode: 'Set to "pip" for picture-in-picture overlay mode.',
38
+ allow_broll: 'Enable AI-generated B-roll cutaway scenes mixed with talking head.',
39
+ broll_model: 'Deprecated. Backend auto-selects the best model. Accepted but ignored.',
40
+ broll_images: 'Array of image URLs for B-roll scenes. Max 10. Use descriptive filenames for better scene matching.',
41
+ product_image_url: 'Product image URL used as default B-roll reference.',
42
+ dub_language: 'BCP-47 language code for dubbing the final video. Example: "es", "fr", "de".',
43
+ scenes: 'Array of scene objects for explicit per-scene control. Bypasses AI scene splitting. Max 30.',
44
+ webhook_url: 'URL to receive a POST callback when the job completes or fails.',
45
+ video_url: 'URL of the video to add subtitles to.',
46
+ angle: 'Review angle/perspective.',
47
+ };
48
+
49
+ // ── Build spec ──────────────────────────────────────────────────────────────
50
+
51
+ const paths: Record<string, unknown> = {};
52
+
53
+ for (const [id, gen] of Object.entries(GENERATORS)) {
54
+ const jsonSchema = zodToJsonSchema(gen.inputSchema, {
55
+ name: `${id}_input`,
56
+ $refStrategy: 'none',
57
+ });
58
+
59
+ const schema = (jsonSchema as any).definitions?.[`${id}_input`] ?? jsonSchema;
60
+
61
+ // Inject descriptions into properties
62
+ if (schema.properties) {
63
+ for (const [key, prop] of Object.entries(schema.properties as Record<string, any>)) {
64
+ if (FIELD_DESCRIPTIONS[key] && !prop.description) {
65
+ prop.description = FIELD_DESCRIPTIONS[key];
66
+ }
67
+ }
68
+ }
69
+
70
+ // Add target_duration constraints
71
+ if (schema.properties?.target_duration) {
72
+ schema.properties.target_duration.minimum = 5;
73
+ schema.properties.target_duration.maximum = 15;
74
+ schema.properties.target_duration.description = FIELD_DESCRIPTIONS.target_duration;
75
+ }
76
+
77
+ paths[`/v1/generate/${id}`] = {
78
+ post: {
79
+ operationId: id,
80
+ summary: gen.description,
81
+ tags: ['generate'],
82
+ security: [{ bearerAuth: [] }],
83
+ requestBody: {
84
+ required: true,
85
+ content: {
86
+ 'application/json': {
87
+ schema,
88
+ example: id === 'ugc_video' ? {
89
+ script: 'Have you ever struggled with creating video content? This tool changed everything for me.',
90
+ actor_slug: 'sofia',
91
+ tone: 'energetic',
92
+ music: 'upbeat',
93
+ style: 'hormozi',
94
+ target_duration: 10,
95
+ aspect_ratio: '9:16',
96
+ } : id === 'subtitle' ? {
97
+ video_url: 'https://example.com/video.mp4',
98
+ style: 'hormozi',
99
+ } : id === 'product_review' ? {
100
+ product_url: 'https://example.com/product',
101
+ angle: 'honest',
102
+ actor_slug: 'marcus',
103
+ } : undefined,
104
+ },
105
+ },
106
+ },
107
+ 'x-codeSamples': id === 'ugc_video' ? [
108
+ {
109
+ lang: 'curl',
110
+ label: 'cURL',
111
+ source: `curl -X POST https://api-v2-production-2f24.up.railway.app/v1/generate/ugc_video \\
112
+ -H "Authorization: Bearer ma_YOUR_KEY" \\
113
+ -H "Content-Type: application/json" \\
114
+ -d '{
115
+ "script": "Have you ever struggled with creating video content? This tool changed everything for me.",
116
+ "actor_slug": "sofia",
117
+ "tone": "energetic",
118
+ "style": "hormozi",
119
+ "target_duration": 10
120
+ }'`,
121
+ },
122
+ {
123
+ lang: 'python',
124
+ label: 'Python',
125
+ source: `from agent_media import AgentMedia
126
+
127
+ client = AgentMedia(api_key="ma_YOUR_KEY")
128
+ video = client.create_video(
129
+ script="Have you ever struggled with creating video content? This tool changed everything for me.",
130
+ actor_slug="sofia",
131
+ tone="energetic",
132
+ style="hormozi",
133
+ target_duration=10,
134
+ )
135
+ print(video["video_url"])`,
136
+ },
137
+ {
138
+ lang: 'typescript',
139
+ label: 'TypeScript',
140
+ source: `import { AgentMedia } from '@agent-media/sdk';
141
+
142
+ const client = new AgentMedia({ apiKey: 'ma_YOUR_KEY' });
143
+ const video = await client.createVideo({
144
+ script: 'Have you ever struggled with creating video content? This tool changed everything for me.',
145
+ actor_slug: 'sofia',
146
+ tone: 'energetic',
147
+ style: 'hormozi',
148
+ target_duration: 10,
149
+ });
150
+ console.log(video.video_url);`,
151
+ },
152
+ ] : undefined,
153
+ responses: {
154
+ '201': {
155
+ description: 'Job submitted successfully',
156
+ content: {
157
+ 'application/json': {
158
+ schema: { $ref: '#/components/schemas/JobSubmitted' },
159
+ },
160
+ },
161
+ },
162
+ '400': {
163
+ description: 'Validation error — request body failed schema validation',
164
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
165
+ },
166
+ '401': {
167
+ description: 'Unauthorized — missing or invalid Bearer token',
168
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
169
+ },
170
+ '402': {
171
+ description: 'Insufficient credits',
172
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/InsufficientCredits' } } },
173
+ },
174
+ '403': {
175
+ description: 'Plan tier insufficient for this operation',
176
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
177
+ },
178
+ '404': {
179
+ description: 'Unknown generator ID',
180
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
181
+ },
182
+ '429': {
183
+ description: 'Rate limit exceeded',
184
+ headers: {
185
+ 'Retry-After': { schema: { type: 'integer' }, description: 'Seconds until rate limit resets' },
186
+ },
187
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
188
+ },
189
+ },
190
+ },
191
+ };
192
+ }
193
+
194
+ // ── Non-generator endpoints ─────────────────────────────────────────────────
195
+
196
+ paths['/v1/actors'] = {
197
+ get: {
198
+ operationId: 'listActors',
199
+ summary: 'List available AI actors for talking head videos',
200
+ description: 'Returns all available actors with their slugs, names, portrait URLs, and demographic info. Use the slug in the actor_slug field when generating videos.',
201
+ tags: ['actors'],
202
+ security: [{ bearerAuth: [] }],
203
+ parameters: [
204
+ { name: 'limit', in: 'query', required: false, schema: { type: 'integer', minimum: 1, maximum: 200, default: 50 }, description: 'Max actors to return (default 50, max 200)' },
205
+ { name: 'offset', in: 'query', required: false, schema: { type: 'integer', minimum: 0, default: 0 }, description: 'Number of actors to skip for pagination' },
206
+ ],
207
+ responses: {
208
+ '200': {
209
+ description: 'List of available actors',
210
+ content: {
211
+ 'application/json': {
212
+ schema: { $ref: '#/components/schemas/ActorList' },
213
+ example: {
214
+ actors: [
215
+ { id: '06fbedfb-853c-43e0-8065-61f7ff953a86', slug: 'sofia', name: 'Sofia', portrait_url: 'https://example.com/sofia.png', age: 32, gender: 'female', nationality: 'Latina' },
216
+ ],
217
+ total: 200,
218
+ limit: 50,
219
+ offset: 0,
220
+ },
221
+ },
222
+ },
223
+ },
224
+ '401': {
225
+ description: 'Unauthorized',
226
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
227
+ },
228
+ },
229
+ },
230
+ };
231
+
232
+ paths['/v1/videos/{jobId}'] = {
233
+ get: {
234
+ operationId: 'getVideoStatus',
235
+ summary: 'Get video generation job status',
236
+ description: 'Poll this endpoint to check if your video is ready. Status progresses: submitted → processing → completed (or failed). When completed, video_url contains the download link.',
237
+ tags: ['videos'],
238
+ security: [{ bearerAuth: [] }],
239
+ parameters: [
240
+ {
241
+ name: 'jobId',
242
+ in: 'path',
243
+ required: true,
244
+ schema: { type: 'string', format: 'uuid' },
245
+ description: 'Job ID returned from the generate endpoint',
246
+ },
247
+ ],
248
+ responses: {
249
+ '200': {
250
+ description: 'Job status and video URL (when completed)',
251
+ content: {
252
+ 'application/json': {
253
+ schema: { $ref: '#/components/schemas/JobStatus' },
254
+ example: {
255
+ job_id: '9ba7ff45-b161-4aba-b14c-fd87d7e19a77',
256
+ status: 'completed',
257
+ progress: {},
258
+ video_url: 'https://pub-16e2ed8f6be84691845e91436920ce0a.r2.dev/generation-outputs/.../ugc-final.mp4',
259
+ error_message: null,
260
+ created_at: '2026-04-11T10:00:00Z',
261
+ updated_at: '2026-04-11T10:03:00Z',
262
+ },
263
+ },
264
+ },
265
+ },
266
+ '404': {
267
+ description: 'Job not found',
268
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
269
+ },
270
+ },
271
+ },
272
+ };
273
+
274
+ // ── Full spec ───────────────────────────────────────────────────────────────
275
+
276
+ const spec = {
277
+ openapi: '3.1.0',
278
+ info: {
279
+ title: 'agent-media API',
280
+ version: '1.0.0',
281
+ description: `AI UGC video production API. Generate talking head videos with lip-synced AI actors, product reviews, and styled subtitles.
282
+
283
+ ## Authentication
284
+ All endpoints require a Bearer token. Use either:
285
+ - **API key**: \`Authorization: Bearer ma_your_key_here\`
286
+ - **Supabase JWT**: \`Authorization: Bearer eyJ...\`
287
+
288
+ ## Async workflow
289
+ Video generation is async. POST to /v1/generate/* returns a job_id immediately. Poll GET /v1/videos/{jobId} until status is "completed" or "failed". Optionally pass webhook_url to receive a callback.
290
+
291
+ ## Rate limits
292
+ - Generate endpoints: 10 requests/minute per user
293
+ - Read endpoints: 60 requests/minute per user`,
294
+ contact: { name: 'agent-media', url: 'https://agent-media.ai' },
295
+ license: { name: 'Apache-2.0', url: 'https://www.apache.org/licenses/LICENSE-2.0' },
296
+ termsOfService: 'https://agent-media.ai/terms',
297
+ },
298
+ servers: [
299
+ { url: 'https://api-v2-production-2f24.up.railway.app', description: 'Production' },
300
+ ],
301
+ paths,
302
+ components: {
303
+ securitySchemes: {
304
+ bearerAuth: {
305
+ type: 'http',
306
+ scheme: 'bearer',
307
+ description: 'API key (ma_xxx prefix) or Supabase JWT token',
308
+ },
309
+ },
310
+ schemas: {
311
+ Error: {
312
+ type: 'object',
313
+ description: 'Standard error response',
314
+ properties: {
315
+ error: {
316
+ type: 'object',
317
+ properties: {
318
+ code: { type: 'string', description: 'Machine-readable error code', example: 'VALIDATION_ERROR' },
319
+ message: { type: 'string', description: 'Human-readable error message', example: 'script: String must contain at least 50 character(s)' },
320
+ details: { type: 'array', items: { type: 'object' }, description: 'Detailed validation errors (Zod issues)' },
321
+ },
322
+ required: ['code', 'message'],
323
+ },
324
+ },
325
+ required: ['error'],
326
+ },
327
+ InsufficientCredits: {
328
+ type: 'object',
329
+ description: 'Returned when user does not have enough credits',
330
+ properties: {
331
+ error: { type: 'string', example: 'insufficient_credits' },
332
+ error_description: { type: 'string', example: 'UGC video (~10s) costs 300 credits, but you only have 50 credits available.' },
333
+ credits_required: { type: 'integer', example: 300 },
334
+ credits_available: { type: 'integer', example: 50 },
335
+ },
336
+ required: ['error', 'error_description', 'credits_required', 'credits_available'],
337
+ },
338
+ JobSubmitted: {
339
+ type: 'object',
340
+ description: 'Returned when a generation job is successfully submitted',
341
+ properties: {
342
+ job_id: { type: 'string', format: 'uuid', description: 'Unique job identifier. Use this to poll status.' },
343
+ status: { type: 'string', enum: ['submitted'], description: 'Always "submitted" for new jobs.' },
344
+ estimated_duration: { type: 'integer', description: 'Estimated video duration in seconds.' },
345
+ credits_deducted: { type: 'integer', description: 'Credits charged for this job.' },
346
+ selected_voice: { type: 'string', description: 'Voice ID used (auto-detected or explicit).' },
347
+ voice_auto_detected: { type: 'boolean', description: 'True if voice was auto-detected from face photo.' },
348
+ word_count: { type: 'integer', description: 'Number of words in the script.' },
349
+ words_per_second: { type: 'number', description: 'Speaking pace (words/sec).' },
350
+ max_words: { type: 'integer', description: 'Max recommended words for the target duration.' },
351
+ pacing_warning: { type: ['string', 'null'], description: 'Warning if script was too long and duration was auto-upgraded.' },
352
+ },
353
+ required: ['job_id', 'status'],
354
+ },
355
+ JobStatus: {
356
+ type: 'object',
357
+ description: 'Video generation job status',
358
+ properties: {
359
+ job_id: { type: 'string', format: 'uuid' },
360
+ status: { type: 'string', enum: ['submitted', 'processing', 'completed', 'failed'], description: 'Current job status.' },
361
+ progress: { type: 'object', description: 'Progress details (provider-specific).' },
362
+ video_url: { type: ['string', 'null'], format: 'uri', description: 'Download URL when status is "completed". Null otherwise.' },
363
+ error_message: { type: ['string', 'null'], description: 'Error details when status is "failed". Null otherwise.' },
364
+ created_at: { type: 'string', format: 'date-time' },
365
+ updated_at: { type: 'string', format: 'date-time' },
366
+ },
367
+ required: ['job_id', 'status'],
368
+ },
369
+ ActorList: {
370
+ type: 'object',
371
+ description: 'Paginated list of available actors',
372
+ properties: {
373
+ actors: {
374
+ type: 'array',
375
+ items: {
376
+ type: 'object',
377
+ properties: {
378
+ id: { type: 'string', format: 'uuid' },
379
+ slug: { type: 'string', description: 'Use this in actor_slug when generating videos.' },
380
+ name: { type: 'string' },
381
+ portrait_url: { type: 'string', format: 'uri' },
382
+ age: { type: 'integer' },
383
+ gender: { type: 'string' },
384
+ nationality: { type: 'string' },
385
+ },
386
+ required: ['id', 'slug', 'name'],
387
+ },
388
+ },
389
+ total: { type: 'integer', description: 'Total number of actors available.' },
390
+ limit: { type: 'integer' },
391
+ offset: { type: 'integer' },
392
+ },
393
+ required: ['actors', 'total'],
394
+ },
395
+ },
396
+ },
397
+ };
398
+
399
+ mkdirSync(outputDir, { recursive: true });
400
+ writeFileSync(outputPath, JSON.stringify(spec, null, 2), 'utf-8');
401
+ console.log(`Generated: ${outputPath}`);
402
+ console.log(`Endpoints: ${Object.keys(paths).length}`);
403
+ console.log(`Generators: ${Object.keys(GENERATORS).length}`);