From 6dbcc4fca924eda2b51a033692c4d33e6c477a24 Mon Sep 17 00:00:00 2001 From: "sdk-github-actions[bot]" Date: Wed, 15 Jul 2026 23:56:46 +0000 Subject: [PATCH 1/3] chore(release): auto --- .sdk.json | 90 +++++++++++++- README.md | 8 ++ package-lock.json | 4 +- package.json | 2 +- src/README.md | 2 + src/environment.ts | 2 +- src/resources/v1/README.md | 2 + src/resources/v1/ai-video-editor/README.md | 72 +++++++++++ src/resources/v1/ai-video-editor/index.ts | 2 + .../v1/ai-video-editor/request-types.ts | 105 ++++++++++++++++ .../v1/ai-video-editor/resource-client.ts | 61 +++++++++ src/resources/v1/body-swap/README.md | 4 +- src/resources/v1/body-swap/resource-client.ts | 2 +- src/resources/v1/character-replace/README.md | 66 ++++++++++ src/resources/v1/character-replace/index.ts | 2 + .../v1/character-replace/request-types.ts | 64 ++++++++++ .../v1/character-replace/resource-client.ts | 63 ++++++++++ src/resources/v1/index.ts | 2 + src/resources/v1/resource-client.ts | 28 +++++ src/types/index.ts | 12 ++ .../v1-ai-video-editor-create-body-assets.ts | 66 ++++++++++ .../v1-ai-video-editor-create-body-style.ts | 61 +++++++++ src/types/v1-ai-video-editor-create-body.ts | 105 ++++++++++++++++ .../v1-ai-video-editor-create-response.ts | 83 +++++++++++++ ...v1-character-replace-create-body-assets.ts | 80 ++++++++++++ ...ter-replace-create-body-resolution-enum.ts | 4 + ...ter-replace-create-body-style-mode-enum.ts | 4 + ...r-replace-create-body-style-points-item.ts | 79 ++++++++++++ ...e-create-body-style-selection-mode-enum.ts | 6 + .../v1-character-replace-create-body-style.ts | 89 +++++++++++++ src/types/v1-character-replace-create-body.ts | 117 ++++++++++++++++++ .../v1-character-replace-create-response.ts | 83 +++++++++++++ test/v1-ai-video-editor.test.ts | 35 ++++++ test/v1-character-replace.test.ts | 21 ++++ 34 files changed, 1418 insertions(+), 8 deletions(-) create mode 100644 src/resources/v1/ai-video-editor/README.md create mode 100644 src/resources/v1/ai-video-editor/index.ts create mode 100644 src/resources/v1/ai-video-editor/request-types.ts create mode 100644 src/resources/v1/ai-video-editor/resource-client.ts create mode 100644 src/resources/v1/character-replace/README.md create mode 100644 src/resources/v1/character-replace/index.ts create mode 100644 src/resources/v1/character-replace/request-types.ts create mode 100644 src/resources/v1/character-replace/resource-client.ts create mode 100644 src/types/v1-ai-video-editor-create-body-assets.ts create mode 100644 src/types/v1-ai-video-editor-create-body-style.ts create mode 100644 src/types/v1-ai-video-editor-create-body.ts create mode 100644 src/types/v1-ai-video-editor-create-response.ts create mode 100644 src/types/v1-character-replace-create-body-assets.ts create mode 100644 src/types/v1-character-replace-create-body-resolution-enum.ts create mode 100644 src/types/v1-character-replace-create-body-style-mode-enum.ts create mode 100644 src/types/v1-character-replace-create-body-style-points-item.ts create mode 100644 src/types/v1-character-replace-create-body-style-selection-mode-enum.ts create mode 100644 src/types/v1-character-replace-create-body-style.ts create mode 100644 src/types/v1-character-replace-create-body.ts create mode 100644 src/types/v1-character-replace-create-response.ts create mode 100644 test/v1-ai-video-editor.test.ts create mode 100644 test/v1-character-replace.test.ts diff --git a/.sdk.json b/.sdk.json index 6a69911..f998df6 100644 --- a/.sdk.json +++ b/.sdk.json @@ -1,5 +1,5 @@ { - "id": "08756a67-c9aa-401d-9e38-feee98a19df4", + "id": "6058b2ba-30f8-46d6-9c9d-83938cbf7071", "tracked_paths": [ { "editable": true, @@ -209,6 +209,22 @@ "editable": true, "path": "src/resources/v1/ai-talking-photo/resource-client.ts" }, + { + "editable": true, + "path": "src/resources/v1/ai-video-editor/README.md" + }, + { + "editable": true, + "path": "src/resources/v1/ai-video-editor/index.ts" + }, + { + "editable": false, + "path": "src/resources/v1/ai-video-editor/request-types.ts" + }, + { + "editable": true, + "path": "src/resources/v1/ai-video-editor/resource-client.ts" + }, { "editable": true, "path": "src/resources/v1/ai-voice-cloner/README.md" @@ -321,6 +337,22 @@ "editable": true, "path": "src/resources/v1/body-swap/resource-client.ts" }, + { + "editable": true, + "path": "src/resources/v1/character-replace/README.md" + }, + { + "editable": true, + "path": "src/resources/v1/character-replace/index.ts" + }, + { + "editable": false, + "path": "src/resources/v1/character-replace/request-types.ts" + }, + { + "editable": true, + "path": "src/resources/v1/character-replace/resource-client.ts" + }, { "editable": true, "path": "src/resources/v1/face-detection/README.md" @@ -765,6 +797,22 @@ "editable": false, "path": "src/types/v1-ai-talking-photo-create-response.ts" }, + { + "editable": false, + "path": "src/types/v1-ai-video-editor-create-body-assets.ts" + }, + { + "editable": false, + "path": "src/types/v1-ai-video-editor-create-body-style.ts" + }, + { + "editable": false, + "path": "src/types/v1-ai-video-editor-create-body.ts" + }, + { + "editable": false, + "path": "src/types/v1-ai-video-editor-create-response.ts" + }, { "editable": false, "path": "src/types/v1-ai-voice-cloner-create-body-assets.ts" @@ -905,6 +953,38 @@ "editable": false, "path": "src/types/v1-body-swap-create-response.ts" }, + { + "editable": false, + "path": "src/types/v1-character-replace-create-body-assets.ts" + }, + { + "editable": false, + "path": "src/types/v1-character-replace-create-body-resolution-enum.ts" + }, + { + "editable": false, + "path": "src/types/v1-character-replace-create-body-style-mode-enum.ts" + }, + { + "editable": false, + "path": "src/types/v1-character-replace-create-body-style-points-item.ts" + }, + { + "editable": false, + "path": "src/types/v1-character-replace-create-body-style-selection-mode-enum.ts" + }, + { + "editable": false, + "path": "src/types/v1-character-replace-create-body-style.ts" + }, + { + "editable": false, + "path": "src/types/v1-character-replace-create-body.ts" + }, + { + "editable": false, + "path": "src/types/v1-character-replace-create-response.ts" + }, { "editable": false, "path": "src/types/v1-face-detection-create-body-assets.ts" @@ -1237,6 +1317,10 @@ "editable": false, "path": "test/v1-ai-talking-photo.test.ts" }, + { + "editable": false, + "path": "test/v1-ai-video-editor.test.ts" + }, { "editable": false, "path": "test/v1-ai-voice-cloner.test.ts" @@ -1265,6 +1349,10 @@ "editable": false, "path": "test/v1-body-swap.test.ts" }, + { + "editable": false, + "path": "test/v1-character-replace.test.ts" + }, { "editable": false, "path": "test/v1-face-detection.test.ts" diff --git a/README.md b/README.md index c660b9e..22dcc45 100644 --- a/README.md +++ b/README.md @@ -251,6 +251,10 @@ Valid values are: `none`, `error`, `warn`, `info`, `debug` (case insensitive). I - [create](src/resources/v1/ai-talking-photo/README.md#create) - AI Talking Photo - [generate](src/resources/v1/ai-talking-photo/README.md#generate) - AI Talking Photo Generate Workflow +### [v1.aiVideoEditor](src/resources/v1/ai-video-editor/README.md) + +- [create](src/resources/v1/ai-video-editor/README.md#create) - AI Video Editor + ### [v1.aiVoiceCloner](src/resources/v1/ai-voice-cloner/README.md) - [create](src/resources/v1/ai-voice-cloner/README.md#create) - AI Voice Cloner @@ -287,6 +291,10 @@ Valid values are: `none`, `error`, `warn`, `info`, `debug` (case insensitive). I - [create](src/resources/v1/body-swap/README.md#create) - Body Swap - [generate](src/resources/v1/body-swap/README.md#generate) - Body Swap Generate Workflow +### [v1.characterReplace](src/resources/v1/character-replace/README.md) + +- [create](src/resources/v1/character-replace/README.md#create) - Character Replace + ### [v1.faceDetection](src/resources/v1/face-detection/README.md) - [create](src/resources/v1/face-detection/README.md#create) - Face Detection diff --git a/package-lock.json b/package-lock.json index e809444..73fb936 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "magic-hour", - "version": "0.67.0", + "version": "0.68.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "magic-hour", - "version": "0.67.0", + "version": "0.68.0", "license": "MIT", "dependencies": { "make-api-request-js": "^0.2.1", diff --git a/package.json b/package.json index 7ff4e59..86ab01e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "magic-hour", - "version": "0.67.0", + "version": "0.68.0", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { diff --git a/src/README.md b/src/README.md index 6a8b85e..9da01a7 100644 --- a/src/README.md +++ b/src/README.md @@ -12,6 +12,7 @@ - [ai-meme-generator](resources/v1/ai-meme-generator/README.md) - ai-meme-generator - [ai-qr-code-generator](resources/v1/ai-qr-code-generator/README.md) - ai-qr-code-generator - [ai-talking-photo](resources/v1/ai-talking-photo/README.md) - ai-talking-photo +- [ai-video-editor](resources/v1/ai-video-editor/README.md) - ai-video-editor - [ai-voice-cloner](resources/v1/ai-voice-cloner/README.md) - ai-voice-cloner - [ai-voice-generator](resources/v1/ai-voice-generator/README.md) - ai-voice-generator - [animation](resources/v1/animation/README.md) - animation @@ -19,6 +20,7 @@ - [audio-to-video](resources/v1/audio-to-video/README.md) - audio-to-video - [auto-subtitle-generator](resources/v1/auto-subtitle-generator/README.md) - auto-subtitle-generator - [body-swap](resources/v1/body-swap/README.md) - body-swap +- [character-replace](resources/v1/character-replace/README.md) - character-replace - [face-detection](resources/v1/face-detection/README.md) - face-detection - [face-swap-photo](resources/v1/face-swap-photo/README.md) - face-swap-photo - [face-swap](resources/v1/face-swap/README.md) - face-swap diff --git a/src/environment.ts b/src/environment.ts index e7419bb..08179dd 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -1,4 +1,4 @@ export enum Environment { Environment = "https://api.magichour.ai", - MockServer = "https://api.sideko.dev/v1/mock/magichour/magic-hour/0.68.0", + MockServer = "https://api.sideko.dev/v1/mock/magichour/magic-hour/0.69.0", } diff --git a/src/resources/v1/README.md b/src/resources/v1/README.md index 3da9c60..adf38aa 100644 --- a/src/resources/v1/README.md +++ b/src/resources/v1/README.md @@ -12,6 +12,7 @@ - [ai-meme-generator](ai-meme-generator/README.md) - ai-meme-generator - [ai-qr-code-generator](ai-qr-code-generator/README.md) - ai-qr-code-generator - [ai-talking-photo](ai-talking-photo/README.md) - ai-talking-photo +- [ai-video-editor](ai-video-editor/README.md) - ai-video-editor - [ai-voice-cloner](ai-voice-cloner/README.md) - ai-voice-cloner - [ai-voice-generator](ai-voice-generator/README.md) - ai-voice-generator - [animation](animation/README.md) - animation @@ -19,6 +20,7 @@ - [audio-to-video](audio-to-video/README.md) - audio-to-video - [auto-subtitle-generator](auto-subtitle-generator/README.md) - auto-subtitle-generator - [body-swap](body-swap/README.md) - body-swap +- [character-replace](character-replace/README.md) - character-replace - [face-detection](face-detection/README.md) - face-detection - [face-swap](face-swap/README.md) - face-swap - [face-swap-photo](face-swap-photo/README.md) - face-swap-photo diff --git a/src/resources/v1/ai-video-editor/README.md b/src/resources/v1/ai-video-editor/README.md new file mode 100644 index 0000000..f588c70 --- /dev/null +++ b/src/resources/v1/ai-video-editor/README.md @@ -0,0 +1,72 @@ +# v1.ai-video-editor + +## Module Functions + +### AI Video Editor + +**What this API does** + +Create the same Video Editor you can make in the browser, but programmatically, so you can automate it, run it at scale, or connect it to your own app or workflow. + +**Good for** + +- Automation and batch processing +- Adding video editor into apps, pipelines, or tools + +**How it works (3 steps)** + +1. Upload your inputs (video, image, or audio) with [Generate Upload URLs](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls) and copy the `file_path`. +2. Send a request to create a video editor job with the basic fields. +3. Check the job status until it's `complete`, then download the result from `downloads`. + +**Key options** + +- Inputs: usually a file, sometimes a YouTube link, depending on project type +- Resolution: free users are limited to 576px; higher plans unlock HD and larger sizes +- Extra fields: e.g. `face_swap_mode`, `start_seconds`/`end_seconds`, or a text prompt + +**Cost**\ +Credits are only charged for the frames that actually render. You'll see an estimate when the job is queued, and the final total after it's done. + +For detailed examples, see the [product page](https://magichour.ai/products/video-editor). + +**API Endpoint**: `POST /v1/ai-video-editor` + +#### Parameters + +| Parameter | Required | Description | Example | +| ------------------ | :------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- | +| `assets` | ✓ | Provide the assets for video editing. | `{"videoFilePath": "api-assets/id/1234.mp4"}` | +| `└─ videoFilePath` | ✓ | The video to edit. This value is either - a direct URL to the video file - `file_path` field from the response of the [upload urls API](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls). See the [file upload guide](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls#input-file) for details. | `"api-assets/id/1234.mp4"` | +| `endSeconds` | ✓ | End time of your clip in seconds. Must be greater than `start_seconds`. Duration must be between 3 and 10 seconds. | `5.0` | +| `style` | ✓ | | `{"prompt": "Change the car color to blue"}` | +| `└─ prompt` | ✓ | The prompt used to edit the video. | `"Change the car color to blue"` | +| `name` | ✗ | Give your video a custom name for easy identification. | `"My Video Editor video"` | +| `startSeconds` | ✗ | Start time of your clip (seconds). Must be ≥ 0. | `0.0` | + +#### Example Snippet + +```typescript +import { Client } from "magic-hour"; + +const client = new Client({ token: process.env["API_TOKEN"]!! }); +const res = await client.v1.aiVideoEditor.create({ + assets: { videoFilePath: "api-assets/id/1234.mp4" }, + endSeconds: 5.0, + name: "My Video Editor video", + startSeconds: 0.0, + style: { prompt: "Change the car color to blue" }, +}); +``` + +#### Response + +##### Type + +[V1AiVideoEditorCreateResponse](/src/types/v1-ai-video-editor-create-response.ts) + +##### Example + +```typescript +{"creditsCharged": 450, "estimatedFrameCost": 450, "id": "cuid-example"} +``` diff --git a/src/resources/v1/ai-video-editor/index.ts b/src/resources/v1/ai-video-editor/index.ts new file mode 100644 index 0000000..01a74ae --- /dev/null +++ b/src/resources/v1/ai-video-editor/index.ts @@ -0,0 +1,2 @@ +export { CreateRequest } from "./request-types"; +export { AiVideoEditorClient } from "./resource-client"; diff --git a/src/resources/v1/ai-video-editor/request-types.ts b/src/resources/v1/ai-video-editor/request-types.ts new file mode 100644 index 0000000..491a149 --- /dev/null +++ b/src/resources/v1/ai-video-editor/request-types.ts @@ -0,0 +1,105 @@ +import { zodTransform } from "make-api-request-js"; +import * as z from "zod"; + +import { + External$V1AiVideoEditorCreateBodyAssets, + Schemas$V1AiVideoEditorCreateBodyAssets, + V1AiVideoEditorCreateBodyAssets, +} from "magic-hour/types/v1-ai-video-editor-create-body-assets"; +import { + External$V1AiVideoEditorCreateBodyStyle, + Schemas$V1AiVideoEditorCreateBodyStyle, + V1AiVideoEditorCreateBodyStyle, +} from "magic-hour/types/v1-ai-video-editor-create-body-style"; + +/** + * CreateRequest + */ +export type CreateRequest = { + /** + * Provide the assets for video editing. + */ + assets: V1AiVideoEditorCreateBodyAssets; + /** + * End time of your clip in seconds. Must be greater than `start_seconds`. Duration must be between 3 and 10 seconds. + */ + endSeconds: number; + /** + * Give your video a custom name for easy identification. + */ + name?: string | undefined; + /** + * Start time of your clip (seconds). Must be ≥ 0. + */ + startSeconds?: number | undefined; + style: V1AiVideoEditorCreateBodyStyle; +}; + +/** + * @internal + * CreateRequest without any key transformation, this is what + * we expect to come in as network data + */ +export type External$CreateRequest = { + assets: External$V1AiVideoEditorCreateBodyAssets; + end_seconds: number; + name?: string | undefined; + start_seconds?: number | undefined; + style: External$V1AiVideoEditorCreateBodyStyle; +}; + +/** + * Takes network data, validates it, and transforms keys to match typescript object CreateRequest + */ +const SchemaIn$CreateRequest: z.ZodType< + CreateRequest, // output type of this zod object + z.ZodTypeDef, + unknown +> = z + .object({ + assets: Schemas$V1AiVideoEditorCreateBodyAssets.in, + end_seconds: z.number(), + name: z.string().optional(), + start_seconds: z.number().optional(), + style: Schemas$V1AiVideoEditorCreateBodyStyle.in, + }) + .transform((obj) => { + return zodTransform(obj, { + assets: "assets", + end_seconds: "endSeconds", + name: "name", + start_seconds: "startSeconds", + style: "style", + }); + }); + +/** + * @internal + * Takes typescript data, validates it, and maps keys to match the expected external object External$CreateRequest + */ +const SchemaOut$CreateRequest: z.ZodType< + External$CreateRequest, // output type of this zod object + z.ZodTypeDef, + CreateRequest // the object to be transformed +> = z + .object({ + assets: Schemas$V1AiVideoEditorCreateBodyAssets.out, + endSeconds: z.number(), + name: z.string().optional(), + startSeconds: z.number().optional(), + style: Schemas$V1AiVideoEditorCreateBodyStyle.out, + }) + .transform((obj) => { + return zodTransform(obj, { + assets: "assets", + endSeconds: "end_seconds", + name: "name", + startSeconds: "start_seconds", + style: "style", + }); + }); + +export const Schemas$CreateRequest = { + in: SchemaIn$CreateRequest, + out: SchemaOut$CreateRequest, +}; diff --git a/src/resources/v1/ai-video-editor/resource-client.ts b/src/resources/v1/ai-video-editor/resource-client.ts new file mode 100644 index 0000000..c5fe538 --- /dev/null +++ b/src/resources/v1/ai-video-editor/resource-client.ts @@ -0,0 +1,61 @@ +import { + ApiPromise, + CoreClient, + CoreResourceClient, + RequestOptions, + ResourceClientOptions, +} from "make-api-request-js"; + +import * as requests from "magic-hour/resources/v1/ai-video-editor/request-types"; +import * as types from "magic-hour/types"; +import { Schemas$V1AiVideoEditorCreateBody } from "magic-hour/types/v1-ai-video-editor-create-body"; +import { Schemas$V1AiVideoEditorCreateResponse } from "magic-hour/types/v1-ai-video-editor-create-response"; + +export class AiVideoEditorClient extends CoreResourceClient { + constructor(coreClient: CoreClient, opts: ResourceClientOptions) { + super(coreClient, opts); + } + + /** + * AI Video Editor + * + * **What this API does** + * + * Create the same Video Editor you can make in the browser, but programmatically, so you can automate it, run it at scale, or connect it to your own app or workflow. + * + * **Good for** + * - Automation and batch processing + * - Adding video editor into apps, pipelines, or tools + * + * **How it works (3 steps)** + * 1) Upload your inputs (video, image, or audio) with [Generate Upload URLs](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls) and copy the `file_path`. + * 2) Send a request to create a video editor job with the basic fields. + * 3) Check the job status until it's `complete`, then download the result from `downloads`. + * + * **Key options** + * - Inputs: usually a file, sometimes a YouTube link, depending on project type + * - Resolution: free users are limited to 576px; higher plans unlock HD and larger sizes + * - Extra fields: e.g. `face_swap_mode`, `start_seconds`/`end_seconds`, or a text prompt + * + * **Cost** + * Credits are only charged for the frames that actually render. You'll see an estimate when the job is queued, and the final total after it's done. + * + * For detailed examples, see the [product page](https://magichour.ai/products/video-editor). + * + * POST /v1/ai-video-editor + */ + create( + request: requests.CreateRequest, + opts?: RequestOptions, + ): ApiPromise { + return this._client.makeRequest({ + method: "post", + path: "/v1/ai-video-editor", + auth: ["bearerAuth"], + contentType: "application/json", + body: Schemas$V1AiVideoEditorCreateBody.out.parse(request), + responseSchema: Schemas$V1AiVideoEditorCreateResponse.in, + opts, + }); + } +} diff --git a/src/resources/v1/body-swap/README.md b/src/resources/v1/body-swap/README.md index 1e32cec..30afb82 100644 --- a/src/resources/v1/body-swap/README.md +++ b/src/resources/v1/body-swap/README.md @@ -51,7 +51,7 @@ const res = await client.v1.bodySwap.generate( ### Body Swap -Swap a person into a scene image using Nano Banana 2. Credits depend on `resolution` (from 100 credits at 640px upward). +Swap a person into a scene image using Nano Banana 2. Credits depend on `resolution` (from 50 credits at 640px upward). **API Endpoint**: `POST /v1/body-swap` @@ -90,5 +90,5 @@ const res = await client.v1.bodySwap.create({ ##### Example ```typescript -{"creditsCharged": 100, "frameCost": 100, "id": "cuid-example"} +{"creditsCharged": 50, "frameCost": 50, "id": "cuid-example"} ``` diff --git a/src/resources/v1/body-swap/resource-client.ts b/src/resources/v1/body-swap/resource-client.ts index 3d710da..3598b67 100644 --- a/src/resources/v1/body-swap/resource-client.ts +++ b/src/resources/v1/body-swap/resource-client.ts @@ -140,7 +140,7 @@ export class BodySwapClient extends CoreResourceClient { /** * Body Swap * - * Swap a person into a scene image using Nano Banana 2. Credits depend on `resolution` (from 100 credits at 640px upward). + * Swap a person into a scene image using Nano Banana 2. Credits depend on `resolution` (from 50 credits at 640px upward). * * POST /v1/body-swap */ diff --git a/src/resources/v1/character-replace/README.md b/src/resources/v1/character-replace/README.md new file mode 100644 index 0000000..1195d09 --- /dev/null +++ b/src/resources/v1/character-replace/README.md @@ -0,0 +1,66 @@ +# v1.character-replace + +## Module Functions + +### Character Replace + +**What this API does** + +Create the same Character Replace you can make in the browser, but programmatically, so you can automate it, run it at scale, or connect it to your own app or workflow. + +**Good for** + +- Automation and batch processing +- Adding character replace into apps, pipelines, or tools + +**How it works (3 steps)** + +1. Upload your inputs (video, image, or audio) with [Generate Upload URLs](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls) and copy the `file_path`. +2. Send a request to create a character replace job with the basic fields. +3. Check the job status until it's `complete`, then download the result from `downloads`. + +**Key options** + +- Inputs: usually a file, sometimes a YouTube link, depending on project type +- Resolution: free users are limited to 576px; higher plans unlock HD and larger sizes +- Extra fields: e.g. `face_swap_mode`, `start_seconds`/`end_seconds`, or a text prompt + +**Cost**\ +Credits are only charged for the frames that actually render. You'll see an estimate when the job is queued, and the final total after it's done. + +For detailed examples, see the [product page](https://magichour.ai/products/character-replace). + +**API Endpoint**: `POST /v1/character-replace` + +#### Parameters + +| Parameter | Required | Description | Example | +| ----------------- | :------: | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `data` | ✗ | | `{"assets": {"imageFilePath": "api-assets/id/5678.png", "videoFilePath": "api-assets/id/1234.mp4"}, "endSeconds": 15.0, "name": "My Character Replace video", "resolution": "720p", "startSeconds": 0.0}` | +| `└─ assets` | ✓ | Source video and reference character image for the job. | `{"imageFilePath": "api-assets/id/5678.png", "videoFilePath": "api-assets/id/1234.mp4"}` | +| `└─ endSeconds` | ✓ | End time of your clip (seconds). Must be greater than start_seconds. | `15.0` | +| `└─ name` | ✗ | Give your video a custom name for easy identification. | `"My Character Replace video"` | +| `└─ resolution` | ✗ | Output video resolution. Defaults to 480p, the lowest resolution available on your plan. | `"720p"` | +| `└─ startSeconds` | ✗ | Start time of your clip (seconds). Must be ≥ 0. | `0.0` | +| `└─ style` | ✗ | Optional style controls for replace vs animate mode and subject selection. | `{"mode": "replace", "selectionMode": "auto"}` | + +#### Example Snippet + +```typescript +import { Client } from "magic-hour"; + +const client = new Client({ token: process.env["API_TOKEN"]!! }); +const res = await client.v1.characterReplace.create(); +``` + +#### Response + +##### Type + +[V1CharacterReplaceCreateResponse](/src/types/v1-character-replace-create-response.ts) + +##### Example + +```typescript +{"creditsCharged": 450, "estimatedFrameCost": 450, "id": "cuid-example"} +``` diff --git a/src/resources/v1/character-replace/index.ts b/src/resources/v1/character-replace/index.ts new file mode 100644 index 0000000..77b5ce7 --- /dev/null +++ b/src/resources/v1/character-replace/index.ts @@ -0,0 +1,2 @@ +export { CreateRequest } from "./request-types"; +export { CharacterReplaceClient } from "./resource-client"; diff --git a/src/resources/v1/character-replace/request-types.ts b/src/resources/v1/character-replace/request-types.ts new file mode 100644 index 0000000..ee085b0 --- /dev/null +++ b/src/resources/v1/character-replace/request-types.ts @@ -0,0 +1,64 @@ +import { zodTransform } from "make-api-request-js"; +import * as z from "zod"; + +import { + External$V1CharacterReplaceCreateBody, + Schemas$V1CharacterReplaceCreateBody, + V1CharacterReplaceCreateBody, +} from "magic-hour/types/v1-character-replace-create-body"; + +/** + * CreateRequest + */ +export type CreateRequest = { + data?: V1CharacterReplaceCreateBody | undefined; +}; + +/** + * @internal + * CreateRequest without any key transformation, this is what + * we expect to come in as network data + */ +export type External$CreateRequest = { + data?: External$V1CharacterReplaceCreateBody | undefined; +}; + +/** + * Takes network data, validates it, and transforms keys to match typescript object CreateRequest + */ +const SchemaIn$CreateRequest: z.ZodType< + CreateRequest, // output type of this zod object + z.ZodTypeDef, + unknown +> = z + .object({ + data: Schemas$V1CharacterReplaceCreateBody.in.optional(), + }) + .transform((obj) => { + return zodTransform(obj, { + data: "data", + }); + }); + +/** + * @internal + * Takes typescript data, validates it, and maps keys to match the expected external object External$CreateRequest + */ +const SchemaOut$CreateRequest: z.ZodType< + External$CreateRequest, // output type of this zod object + z.ZodTypeDef, + CreateRequest // the object to be transformed +> = z + .object({ + data: Schemas$V1CharacterReplaceCreateBody.out.optional(), + }) + .transform((obj) => { + return zodTransform(obj, { + data: "data", + }); + }); + +export const Schemas$CreateRequest = { + in: SchemaIn$CreateRequest, + out: SchemaOut$CreateRequest, +}; diff --git a/src/resources/v1/character-replace/resource-client.ts b/src/resources/v1/character-replace/resource-client.ts new file mode 100644 index 0000000..c8396dd --- /dev/null +++ b/src/resources/v1/character-replace/resource-client.ts @@ -0,0 +1,63 @@ +import { + ApiPromise, + CoreClient, + CoreResourceClient, + RequestOptions, + ResourceClientOptions, +} from "make-api-request-js"; + +import * as requests from "magic-hour/resources/v1/character-replace/request-types"; +import * as types from "magic-hour/types"; +import { Schemas$V1CharacterReplaceCreateBody } from "magic-hour/types/v1-character-replace-create-body"; +import { Schemas$V1CharacterReplaceCreateResponse } from "magic-hour/types/v1-character-replace-create-response"; + +export class CharacterReplaceClient extends CoreResourceClient { + constructor(coreClient: CoreClient, opts: ResourceClientOptions) { + super(coreClient, opts); + } + + /** + * Character Replace + * + * **What this API does** + * + * Create the same Character Replace you can make in the browser, but programmatically, so you can automate it, run it at scale, or connect it to your own app or workflow. + * + * **Good for** + * - Automation and batch processing + * - Adding character replace into apps, pipelines, or tools + * + * **How it works (3 steps)** + * 1) Upload your inputs (video, image, or audio) with [Generate Upload URLs](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls) and copy the `file_path`. + * 2) Send a request to create a character replace job with the basic fields. + * 3) Check the job status until it's `complete`, then download the result from `downloads`. + * + * **Key options** + * - Inputs: usually a file, sometimes a YouTube link, depending on project type + * - Resolution: free users are limited to 576px; higher plans unlock HD and larger sizes + * - Extra fields: e.g. `face_swap_mode`, `start_seconds`/`end_seconds`, or a text prompt + * + * **Cost** + * Credits are only charged for the frames that actually render. You'll see an estimate when the job is queued, and the final total after it's done. + * + * For detailed examples, see the [product page](https://magichour.ai/products/character-replace). + * + * POST /v1/character-replace + */ + create( + request: requests.CreateRequest = {}, + opts?: RequestOptions, + ): ApiPromise { + return this._client.makeRequest({ + method: "post", + path: "/v1/character-replace", + auth: ["bearerAuth"], + contentType: "application/json", + body: request.data + ? Schemas$V1CharacterReplaceCreateBody.out.parse(request.data) + : undefined, + responseSchema: Schemas$V1CharacterReplaceCreateResponse.in, + opts, + }); + } +} diff --git a/src/resources/v1/index.ts b/src/resources/v1/index.ts index 8ab8be0..ac5d7f3 100644 --- a/src/resources/v1/index.ts +++ b/src/resources/v1/index.ts @@ -8,6 +8,7 @@ export * as aiImageUpscaler from "./ai-image-upscaler"; export * as aiMemeGenerator from "./ai-meme-generator"; export * as aiQrCodeGenerator from "./ai-qr-code-generator"; export * as aiTalkingPhoto from "./ai-talking-photo"; +export * as aiVideoEditor from "./ai-video-editor"; export * as aiVoiceCloner from "./ai-voice-cloner"; export * as aiVoiceGenerator from "./ai-voice-generator"; export * as animation from "./animation"; @@ -15,6 +16,7 @@ export * as audioProjects from "./audio-projects"; export * as audioToVideo from "./audio-to-video"; export * as autoSubtitleGenerator from "./auto-subtitle-generator"; export * as bodySwap from "./body-swap"; +export * as characterReplace from "./character-replace"; export * as faceDetection from "./face-detection"; export * as faceSwap from "./face-swap"; export * as faceSwapPhoto from "./face-swap-photo"; diff --git a/src/resources/v1/resource-client.ts b/src/resources/v1/resource-client.ts index 10ba16a..f829e3a 100644 --- a/src/resources/v1/resource-client.ts +++ b/src/resources/v1/resource-client.ts @@ -14,6 +14,7 @@ import { AiImageUpscalerClient } from "magic-hour/resources/v1/ai-image-upscaler import { AiMemeGeneratorClient } from "magic-hour/resources/v1/ai-meme-generator"; import { AiQrCodeGeneratorClient } from "magic-hour/resources/v1/ai-qr-code-generator"; import { AiTalkingPhotoClient } from "magic-hour/resources/v1/ai-talking-photo"; +import { AiVideoEditorClient } from "magic-hour/resources/v1/ai-video-editor"; import { AiVoiceClonerClient } from "magic-hour/resources/v1/ai-voice-cloner"; import { AiVoiceGeneratorClient } from "magic-hour/resources/v1/ai-voice-generator"; import { AnimationClient } from "magic-hour/resources/v1/animation"; @@ -21,6 +22,7 @@ import { AudioProjectsClient } from "magic-hour/resources/v1/audio-projects"; import { AudioToVideoClient } from "magic-hour/resources/v1/audio-to-video"; import { AutoSubtitleGeneratorClient } from "magic-hour/resources/v1/auto-subtitle-generator"; import { BodySwapClient } from "magic-hour/resources/v1/body-swap"; +import { CharacterReplaceClient } from "magic-hour/resources/v1/character-replace"; import { FaceDetectionClient } from "magic-hour/resources/v1/face-detection"; import { FaceSwapClient } from "magic-hour/resources/v1/face-swap"; import { FaceSwapPhotoClient } from "magic-hour/resources/v1/face-swap-photo"; @@ -36,6 +38,8 @@ import { VideoProjectsClient } from "magic-hour/resources/v1/video-projects"; import { VideoToVideoClient } from "magic-hour/resources/v1/video-to-video"; export class V1Client extends CoreResourceClient { + private _characterReplaceLazy?: CharacterReplaceClient; // lazy-loading cache + private _aiVideoEditorLazy?: AiVideoEditorClient; // lazy-loading cache private _audioToVideoLazy?: AudioToVideoClient; // lazy-loading cache private _bodySwapLazy?: BodySwapClient; // lazy-loading cache private _headSwapLazy?: HeadSwapClient; // lazy-loading cache @@ -81,6 +85,7 @@ export class V1Client extends CoreResourceClient { this.aiMemeGenerator; this.aiQrCodeGenerator; this.aiTalkingPhoto; + this.aiVideoEditor; this.aiVoiceCloner; this.aiVoiceGenerator; this.animation; @@ -88,6 +93,7 @@ export class V1Client extends CoreResourceClient { this.audioToVideo; this.autoSubtitleGenerator; this.bodySwap; + this.characterReplace; this.faceDetection; this.faceSwap; this.faceSwapPhoto; @@ -427,4 +433,26 @@ export class V1Client extends CoreResourceClient { )) ); } + + get aiVideoEditor(): AiVideoEditorClient { + return ( + this._aiVideoEditorLazy ?? + (this._aiVideoEditorLazy = + new (require("./ai-video-editor").AiVideoEditorClient)( + this._client, + this._opts, + )) + ); + } + + get characterReplace(): CharacterReplaceClient { + return ( + this._characterReplaceLazy ?? + (this._characterReplaceLazy = + new (require("./character-replace").CharacterReplaceClient)( + this._client, + this._opts, + )) + ); + } } diff --git a/src/types/index.ts b/src/types/index.ts index d0e42d3..dd29689 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -49,6 +49,10 @@ export { V1AiTalkingPhotoCreateBodyAssets } from "./v1-ai-talking-photo-create-b export { V1AiTalkingPhotoCreateBodyStyle } from "./v1-ai-talking-photo-create-body-style"; export { V1AiTalkingPhotoCreateBodyStyleGenerationModeEnum } from "./v1-ai-talking-photo-create-body-style-generation-mode-enum"; export { V1AiTalkingPhotoCreateResponse } from "./v1-ai-talking-photo-create-response"; +export { V1AiVideoEditorCreateBody } from "./v1-ai-video-editor-create-body"; +export { V1AiVideoEditorCreateBodyAssets } from "./v1-ai-video-editor-create-body-assets"; +export { V1AiVideoEditorCreateBodyStyle } from "./v1-ai-video-editor-create-body-style"; +export { V1AiVideoEditorCreateResponse } from "./v1-ai-video-editor-create-response"; export { V1AiVoiceClonerCreateBody } from "./v1-ai-voice-cloner-create-body"; export { V1AiVoiceClonerCreateBodyAssets } from "./v1-ai-voice-cloner-create-body-assets"; export { V1AiVoiceClonerCreateBodyStyle } from "./v1-ai-voice-cloner-create-body-style"; @@ -84,6 +88,14 @@ export { V1BodySwapCreateBody } from "./v1-body-swap-create-body"; export { V1BodySwapCreateBodyAssets } from "./v1-body-swap-create-body-assets"; export { V1BodySwapCreateBodyResolutionEnum } from "./v1-body-swap-create-body-resolution-enum"; export { V1BodySwapCreateResponse } from "./v1-body-swap-create-response"; +export { V1CharacterReplaceCreateBody } from "./v1-character-replace-create-body"; +export { V1CharacterReplaceCreateBodyAssets } from "./v1-character-replace-create-body-assets"; +export { V1CharacterReplaceCreateBodyResolutionEnum } from "./v1-character-replace-create-body-resolution-enum"; +export { V1CharacterReplaceCreateBodyStyle } from "./v1-character-replace-create-body-style"; +export { V1CharacterReplaceCreateBodyStyleModeEnum } from "./v1-character-replace-create-body-style-mode-enum"; +export { V1CharacterReplaceCreateBodyStylePointsItem } from "./v1-character-replace-create-body-style-points-item"; +export { V1CharacterReplaceCreateBodyStyleSelectionModeEnum } from "./v1-character-replace-create-body-style-selection-mode-enum"; +export { V1CharacterReplaceCreateResponse } from "./v1-character-replace-create-response"; export { V1FaceDetectionCreateBody } from "./v1-face-detection-create-body"; export { V1FaceDetectionCreateBodyAssets } from "./v1-face-detection-create-body-assets"; export { V1FaceDetectionCreateResponse } from "./v1-face-detection-create-response"; diff --git a/src/types/v1-ai-video-editor-create-body-assets.ts b/src/types/v1-ai-video-editor-create-body-assets.ts new file mode 100644 index 0000000..7366f18 --- /dev/null +++ b/src/types/v1-ai-video-editor-create-body-assets.ts @@ -0,0 +1,66 @@ +import { zodTransform } from "make-api-request-js"; +import * as z from "zod"; + +/** + * Provide the assets for video editing. + */ +export type V1AiVideoEditorCreateBodyAssets = { + /** + * The video to edit. This value is either + * - a direct URL to the video file + * - `file_path` field from the response of the [upload urls API](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls). + * + * See the [file upload guide](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls#input-file) for details. + * + */ + videoFilePath: string; +}; + +/** + * @internal + * V1AiVideoEditorCreateBodyAssets without any key transformation, this is what + * we expect to come in as network data + */ +export type External$V1AiVideoEditorCreateBodyAssets = { + video_file_path: string; +}; + +/** + * Takes network data, validates it, and transforms keys to match typescript object V1AiVideoEditorCreateBodyAssets + */ +const SchemaIn$V1AiVideoEditorCreateBodyAssets: z.ZodType< + V1AiVideoEditorCreateBodyAssets, // output type of this zod object + z.ZodTypeDef, + unknown +> = z + .object({ + video_file_path: z.string(), + }) + .transform((obj) => { + return zodTransform(obj, { + video_file_path: "videoFilePath", + }); + }); + +/** + * @internal + * Takes typescript data, validates it, and maps keys to match the expected external object External$V1AiVideoEditorCreateBodyAssets + */ +const SchemaOut$V1AiVideoEditorCreateBodyAssets: z.ZodType< + External$V1AiVideoEditorCreateBodyAssets, // output type of this zod object + z.ZodTypeDef, + V1AiVideoEditorCreateBodyAssets // the object to be transformed +> = z + .object({ + videoFilePath: z.string(), + }) + .transform((obj) => { + return zodTransform(obj, { + videoFilePath: "video_file_path", + }); + }); + +export const Schemas$V1AiVideoEditorCreateBodyAssets = { + in: SchemaIn$V1AiVideoEditorCreateBodyAssets, + out: SchemaOut$V1AiVideoEditorCreateBodyAssets, +}; diff --git a/src/types/v1-ai-video-editor-create-body-style.ts b/src/types/v1-ai-video-editor-create-body-style.ts new file mode 100644 index 0000000..c030148 --- /dev/null +++ b/src/types/v1-ai-video-editor-create-body-style.ts @@ -0,0 +1,61 @@ +import { zodTransform } from "make-api-request-js"; +import * as z from "zod"; + +/** + * V1AiVideoEditorCreateBodyStyle + */ +export type V1AiVideoEditorCreateBodyStyle = { + /** + * The prompt used to edit the video. + */ + prompt: string; +}; + +/** + * @internal + * V1AiVideoEditorCreateBodyStyle without any key transformation, this is what + * we expect to come in as network data + */ +export type External$V1AiVideoEditorCreateBodyStyle = { + prompt: string; +}; + +/** + * Takes network data, validates it, and transforms keys to match typescript object V1AiVideoEditorCreateBodyStyle + */ +const SchemaIn$V1AiVideoEditorCreateBodyStyle: z.ZodType< + V1AiVideoEditorCreateBodyStyle, // output type of this zod object + z.ZodTypeDef, + unknown +> = z + .object({ + prompt: z.string(), + }) + .transform((obj) => { + return zodTransform(obj, { + prompt: "prompt", + }); + }); + +/** + * @internal + * Takes typescript data, validates it, and maps keys to match the expected external object External$V1AiVideoEditorCreateBodyStyle + */ +const SchemaOut$V1AiVideoEditorCreateBodyStyle: z.ZodType< + External$V1AiVideoEditorCreateBodyStyle, // output type of this zod object + z.ZodTypeDef, + V1AiVideoEditorCreateBodyStyle // the object to be transformed +> = z + .object({ + prompt: z.string(), + }) + .transform((obj) => { + return zodTransform(obj, { + prompt: "prompt", + }); + }); + +export const Schemas$V1AiVideoEditorCreateBodyStyle = { + in: SchemaIn$V1AiVideoEditorCreateBodyStyle, + out: SchemaOut$V1AiVideoEditorCreateBodyStyle, +}; diff --git a/src/types/v1-ai-video-editor-create-body.ts b/src/types/v1-ai-video-editor-create-body.ts new file mode 100644 index 0000000..6d26e12 --- /dev/null +++ b/src/types/v1-ai-video-editor-create-body.ts @@ -0,0 +1,105 @@ +import { zodTransform } from "make-api-request-js"; +import * as z from "zod"; + +import { + External$V1AiVideoEditorCreateBodyAssets, + Schemas$V1AiVideoEditorCreateBodyAssets, + V1AiVideoEditorCreateBodyAssets, +} from "./v1-ai-video-editor-create-body-assets"; +import { + External$V1AiVideoEditorCreateBodyStyle, + Schemas$V1AiVideoEditorCreateBodyStyle, + V1AiVideoEditorCreateBodyStyle, +} from "./v1-ai-video-editor-create-body-style"; + +/** + * V1AiVideoEditorCreateBody + */ +export type V1AiVideoEditorCreateBody = { + /** + * Provide the assets for video editing. + */ + assets: V1AiVideoEditorCreateBodyAssets; + /** + * End time of your clip in seconds. Must be greater than `start_seconds`. Duration must be between 3 and 10 seconds. + */ + endSeconds: number; + /** + * Give your video a custom name for easy identification. + */ + name?: string | undefined; + /** + * Start time of your clip (seconds). Must be ≥ 0. + */ + startSeconds?: number | undefined; + style: V1AiVideoEditorCreateBodyStyle; +}; + +/** + * @internal + * V1AiVideoEditorCreateBody without any key transformation, this is what + * we expect to come in as network data + */ +export type External$V1AiVideoEditorCreateBody = { + assets: External$V1AiVideoEditorCreateBodyAssets; + end_seconds: number; + name?: string | undefined; + start_seconds?: number | undefined; + style: External$V1AiVideoEditorCreateBodyStyle; +}; + +/** + * Takes network data, validates it, and transforms keys to match typescript object V1AiVideoEditorCreateBody + */ +const SchemaIn$V1AiVideoEditorCreateBody: z.ZodType< + V1AiVideoEditorCreateBody, // output type of this zod object + z.ZodTypeDef, + unknown +> = z + .object({ + assets: Schemas$V1AiVideoEditorCreateBodyAssets.in, + end_seconds: z.number(), + name: z.string().optional(), + start_seconds: z.number().optional(), + style: Schemas$V1AiVideoEditorCreateBodyStyle.in, + }) + .transform((obj) => { + return zodTransform(obj, { + assets: "assets", + end_seconds: "endSeconds", + name: "name", + start_seconds: "startSeconds", + style: "style", + }); + }); + +/** + * @internal + * Takes typescript data, validates it, and maps keys to match the expected external object External$V1AiVideoEditorCreateBody + */ +const SchemaOut$V1AiVideoEditorCreateBody: z.ZodType< + External$V1AiVideoEditorCreateBody, // output type of this zod object + z.ZodTypeDef, + V1AiVideoEditorCreateBody // the object to be transformed +> = z + .object({ + assets: Schemas$V1AiVideoEditorCreateBodyAssets.out, + endSeconds: z.number(), + name: z.string().optional(), + startSeconds: z.number().optional(), + style: Schemas$V1AiVideoEditorCreateBodyStyle.out, + }) + .transform((obj) => { + return zodTransform(obj, { + assets: "assets", + endSeconds: "end_seconds", + name: "name", + startSeconds: "start_seconds", + style: "style", + }); + }); + +export const Schemas$V1AiVideoEditorCreateBody = { + in: SchemaIn$V1AiVideoEditorCreateBody, + out: SchemaOut$V1AiVideoEditorCreateBody, +}; diff --git a/src/types/v1-ai-video-editor-create-response.ts b/src/types/v1-ai-video-editor-create-response.ts new file mode 100644 index 0000000..a610d35 --- /dev/null +++ b/src/types/v1-ai-video-editor-create-response.ts @@ -0,0 +1,83 @@ +import { zodTransform } from "make-api-request-js"; +import * as z from "zod"; + +/** + * Success + */ +export type V1AiVideoEditorCreateResponse = { + /** + * The amount of credits deducted from your account to generate the video. If the status is not 'complete', this value is an estimate and may be adjusted upon completion based on the actual FPS of the output video. + * + * If video generation fails, credits will be refunded, and this field will be updated to include the refund. + */ + creditsCharged: number; + /** + * Deprecated: Previously represented the number of frames (original name of our credit system) used for video generation. Use 'credits_charged' instead. + * + * The amount of frames used to generate the video. If the status is not 'complete', the cost is an estimate and will be adjusted when the video completes. + */ + estimatedFrameCost: number; + /** + * Unique ID of the video. Use it with the [Get video Project API](https://docs.magichour.ai/api-reference/video-projects/get-video-details) to fetch status and downloads. + */ + id: string; +}; + +/** + * @internal + * V1AiVideoEditorCreateResponse without any key transformation, this is what + * we expect to come in as network data + */ +export type External$V1AiVideoEditorCreateResponse = { + credits_charged: number; + estimated_frame_cost: number; + id: string; +}; + +/** + * Takes network data, validates it, and transforms keys to match typescript object V1AiVideoEditorCreateResponse + */ +const SchemaIn$V1AiVideoEditorCreateResponse: z.ZodType< + V1AiVideoEditorCreateResponse, // output type of this zod object + z.ZodTypeDef, + unknown +> = z + .object({ + credits_charged: z.number().int(), + estimated_frame_cost: z.number().int(), + id: z.string(), + }) + .transform((obj) => { + return zodTransform(obj, { + credits_charged: "creditsCharged", + estimated_frame_cost: "estimatedFrameCost", + id: "id", + }); + }); + +/** + * @internal + * Takes typescript data, validates it, and maps keys to match the expected external object External$V1AiVideoEditorCreateResponse + */ +const SchemaOut$V1AiVideoEditorCreateResponse: z.ZodType< + External$V1AiVideoEditorCreateResponse, // output type of this zod object + z.ZodTypeDef, + V1AiVideoEditorCreateResponse // the object to be transformed +> = z + .object({ + creditsCharged: z.number().int(), + estimatedFrameCost: z.number().int(), + id: z.string(), + }) + .transform((obj) => { + return zodTransform(obj, { + creditsCharged: "credits_charged", + estimatedFrameCost: "estimated_frame_cost", + id: "id", + }); + }); + +export const Schemas$V1AiVideoEditorCreateResponse = { + in: SchemaIn$V1AiVideoEditorCreateResponse, + out: SchemaOut$V1AiVideoEditorCreateResponse, +}; diff --git a/src/types/v1-character-replace-create-body-assets.ts b/src/types/v1-character-replace-create-body-assets.ts new file mode 100644 index 0000000..773bd08 --- /dev/null +++ b/src/types/v1-character-replace-create-body-assets.ts @@ -0,0 +1,80 @@ +import { zodTransform } from "make-api-request-js"; +import * as z from "zod"; + +/** + * Source video and reference character image for the job. + */ +export type V1CharacterReplaceCreateBodyAssets = { + /** + * Reference character image used as the replacement or animation target. This value is either + * - a direct URL to the video file + * - `file_path` field from the response of the [upload urls API](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls). + * + * See the [file upload guide](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls#input-file) for details. + * + */ + imageFilePath: string; + /** + * Source video containing the subject to replace or animate. This value is either + * - a direct URL to the video file + * - `file_path` field from the response of the [upload urls API](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls). + * + * See the [file upload guide](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls#input-file) for details. + * + */ + videoFilePath: string; +}; + +/** + * @internal + * V1CharacterReplaceCreateBodyAssets without any key transformation, this is what + * we expect to come in as network data + */ +export type External$V1CharacterReplaceCreateBodyAssets = { + image_file_path: string; + video_file_path: string; +}; + +/** + * Takes network data, validates it, and transforms keys to match typescript object V1CharacterReplaceCreateBodyAssets + */ +const SchemaIn$V1CharacterReplaceCreateBodyAssets: z.ZodType< + V1CharacterReplaceCreateBodyAssets, // output type of this zod object + z.ZodTypeDef, + unknown +> = z + .object({ + image_file_path: z.string(), + video_file_path: z.string(), + }) + .transform((obj) => { + return zodTransform(obj, { + image_file_path: "imageFilePath", + video_file_path: "videoFilePath", + }); + }); + +/** + * @internal + * Takes typescript data, validates it, and maps keys to match the expected external object External$V1CharacterReplaceCreateBodyAssets + */ +const SchemaOut$V1CharacterReplaceCreateBodyAssets: z.ZodType< + External$V1CharacterReplaceCreateBodyAssets, // output type of this zod object + z.ZodTypeDef, + V1CharacterReplaceCreateBodyAssets // the object to be transformed +> = z + .object({ + imageFilePath: z.string(), + videoFilePath: z.string(), + }) + .transform((obj) => { + return zodTransform(obj, { + imageFilePath: "image_file_path", + videoFilePath: "video_file_path", + }); + }); + +export const Schemas$V1CharacterReplaceCreateBodyAssets = { + in: SchemaIn$V1CharacterReplaceCreateBodyAssets, + out: SchemaOut$V1CharacterReplaceCreateBodyAssets, +}; diff --git a/src/types/v1-character-replace-create-body-resolution-enum.ts b/src/types/v1-character-replace-create-body-resolution-enum.ts new file mode 100644 index 0000000..af3196c --- /dev/null +++ b/src/types/v1-character-replace-create-body-resolution-enum.ts @@ -0,0 +1,4 @@ +/** + * Output video resolution. Defaults to 480p, the lowest resolution available on your plan. + */ +export type V1CharacterReplaceCreateBodyResolutionEnum = "480p" | "720p"; diff --git a/src/types/v1-character-replace-create-body-style-mode-enum.ts b/src/types/v1-character-replace-create-body-style-mode-enum.ts new file mode 100644 index 0000000..f80d878 --- /dev/null +++ b/src/types/v1-character-replace-create-body-style-mode-enum.ts @@ -0,0 +1,4 @@ +/** + * Processing mode. `replace` swaps the detected subject with your reference character. `animate` transfers motion from the video onto your character image. + */ +export type V1CharacterReplaceCreateBodyStyleModeEnum = "animate" | "replace"; diff --git a/src/types/v1-character-replace-create-body-style-points-item.ts b/src/types/v1-character-replace-create-body-style-points-item.ts new file mode 100644 index 0000000..fd596c9 --- /dev/null +++ b/src/types/v1-character-replace-create-body-style-points-item.ts @@ -0,0 +1,79 @@ +import { zodTransform } from "make-api-request-js"; +import * as z from "zod"; + +/** + * V1CharacterReplaceCreateBodyStylePointsItem + */ +export type V1CharacterReplaceCreateBodyStylePointsItem = { + /** + * Horizontal pixel coordinate in the source video frame at `time_seconds`, measured from the left edge. + */ + positionX: number; + /** + * Vertical pixel coordinate in the source video frame at `time_seconds`, measured from the top edge. + */ + positionY: number; + /** + * Timestamp on the source video timeline in seconds. Uses the same clock as `start_seconds` and `end_seconds`. + */ + timeSeconds: number; +}; + +/** + * @internal + * V1CharacterReplaceCreateBodyStylePointsItem without any key transformation, this is what + * we expect to come in as network data + */ +export type External$V1CharacterReplaceCreateBodyStylePointsItem = { + position_x: number; + position_y: number; + time_seconds: number; +}; + +/** + * Takes network data, validates it, and transforms keys to match typescript object V1CharacterReplaceCreateBodyStylePointsItem + */ +const SchemaIn$V1CharacterReplaceCreateBodyStylePointsItem: z.ZodType< + V1CharacterReplaceCreateBodyStylePointsItem, // output type of this zod object + z.ZodTypeDef, + unknown +> = z + .object({ + position_x: z.number().int(), + position_y: z.number().int(), + time_seconds: z.number(), + }) + .transform((obj) => { + return zodTransform(obj, { + position_x: "positionX", + position_y: "positionY", + time_seconds: "timeSeconds", + }); + }); + +/** + * @internal + * Takes typescript data, validates it, and maps keys to match the expected external object External$V1CharacterReplaceCreateBodyStylePointsItem + */ +const SchemaOut$V1CharacterReplaceCreateBodyStylePointsItem: z.ZodType< + External$V1CharacterReplaceCreateBodyStylePointsItem, // output type of this zod object + z.ZodTypeDef, + V1CharacterReplaceCreateBodyStylePointsItem // the object to be transformed +> = z + .object({ + positionX: z.number().int(), + positionY: z.number().int(), + timeSeconds: z.number(), + }) + .transform((obj) => { + return zodTransform(obj, { + positionX: "position_x", + positionY: "position_y", + timeSeconds: "time_seconds", + }); + }); + +export const Schemas$V1CharacterReplaceCreateBodyStylePointsItem = { + in: SchemaIn$V1CharacterReplaceCreateBodyStylePointsItem, + out: SchemaOut$V1CharacterReplaceCreateBodyStylePointsItem, +}; diff --git a/src/types/v1-character-replace-create-body-style-selection-mode-enum.ts b/src/types/v1-character-replace-create-body-style-selection-mode-enum.ts new file mode 100644 index 0000000..7d45ef9 --- /dev/null +++ b/src/types/v1-character-replace-create-body-style-selection-mode-enum.ts @@ -0,0 +1,6 @@ +/** + * How to locate the subject in the source video. `auto` detects a person automatically. `point` uses your `points` to mark the subject. Defaults to `auto`. + */ +export type V1CharacterReplaceCreateBodyStyleSelectionModeEnum = + | "auto" + | "point"; diff --git a/src/types/v1-character-replace-create-body-style.ts b/src/types/v1-character-replace-create-body-style.ts new file mode 100644 index 0000000..cdafcb8 --- /dev/null +++ b/src/types/v1-character-replace-create-body-style.ts @@ -0,0 +1,89 @@ +import { zodTransform } from "make-api-request-js"; +import * as z from "zod"; + +import { + External$V1CharacterReplaceCreateBodyStylePointsItem, + Schemas$V1CharacterReplaceCreateBodyStylePointsItem, + V1CharacterReplaceCreateBodyStylePointsItem, +} from "./v1-character-replace-create-body-style-points-item"; + +/** + * Optional style controls for replace vs animate mode and subject selection. + */ +export type V1CharacterReplaceCreateBodyStyle = { + /** + * Processing mode. `replace` swaps the detected subject with your reference character. `animate` transfers motion from the video onto your character image. + */ + mode?: ("animate" | "replace") | undefined; + /** + * On-frame markers for manual subject selection. Required when `selection_mode` is `point`. Ignored when `selection_mode` is `auto` or omitted. + */ + points?: V1CharacterReplaceCreateBodyStylePointsItem[] | undefined; + /** + * How to locate the subject in the source video. `auto` detects a person automatically. `point` uses your `points` to mark the subject. Defaults to `auto`. + */ + selectionMode?: ("auto" | "point") | undefined; +}; + +/** + * @internal + * V1CharacterReplaceCreateBodyStyle without any key transformation, this is what + * we expect to come in as network data + */ +export type External$V1CharacterReplaceCreateBodyStyle = { + mode?: ("animate" | "replace") | undefined; + points?: External$V1CharacterReplaceCreateBodyStylePointsItem[] | undefined; + selection_mode?: ("auto" | "point") | undefined; +}; + +/** + * Takes network data, validates it, and transforms keys to match typescript object V1CharacterReplaceCreateBodyStyle + */ +const SchemaIn$V1CharacterReplaceCreateBodyStyle: z.ZodType< + V1CharacterReplaceCreateBodyStyle, // output type of this zod object + z.ZodTypeDef, + unknown +> = z + .object({ + mode: z.enum(["animate", "replace"]).optional(), + points: z + .array(Schemas$V1CharacterReplaceCreateBodyStylePointsItem.in) + .optional(), + selection_mode: z.enum(["auto", "point"]).optional(), + }) + .transform((obj) => { + return zodTransform(obj, { + mode: "mode", + points: "points", + selection_mode: "selectionMode", + }); + }); + +/** + * @internal + * Takes typescript data, validates it, and maps keys to match the expected external object External$V1CharacterReplaceCreateBodyStyle + */ +const SchemaOut$V1CharacterReplaceCreateBodyStyle: z.ZodType< + External$V1CharacterReplaceCreateBodyStyle, // output type of this zod object + z.ZodTypeDef, + V1CharacterReplaceCreateBodyStyle // the object to be transformed +> = z + .object({ + mode: z.enum(["animate", "replace"]).optional(), + points: z + .array(Schemas$V1CharacterReplaceCreateBodyStylePointsItem.out) + .optional(), + selectionMode: z.enum(["auto", "point"]).optional(), + }) + .transform((obj) => { + return zodTransform(obj, { + mode: "mode", + points: "points", + selectionMode: "selection_mode", + }); + }); + +export const Schemas$V1CharacterReplaceCreateBodyStyle = { + in: SchemaIn$V1CharacterReplaceCreateBodyStyle, + out: SchemaOut$V1CharacterReplaceCreateBodyStyle, +}; diff --git a/src/types/v1-character-replace-create-body.ts b/src/types/v1-character-replace-create-body.ts new file mode 100644 index 0000000..dbf0fa9 --- /dev/null +++ b/src/types/v1-character-replace-create-body.ts @@ -0,0 +1,117 @@ +import { zodTransform } from "make-api-request-js"; +import * as z from "zod"; + +import { + External$V1CharacterReplaceCreateBodyAssets, + Schemas$V1CharacterReplaceCreateBodyAssets, + V1CharacterReplaceCreateBodyAssets, +} from "./v1-character-replace-create-body-assets"; +import { + External$V1CharacterReplaceCreateBodyStyle, + Schemas$V1CharacterReplaceCreateBodyStyle, + V1CharacterReplaceCreateBodyStyle, +} from "./v1-character-replace-create-body-style"; + +/** + * V1CharacterReplaceCreateBody + */ +export type V1CharacterReplaceCreateBody = { + /** + * Source video and reference character image for the job. + */ + assets: V1CharacterReplaceCreateBodyAssets; + /** + * End time of your clip (seconds). Must be greater than start_seconds. + */ + endSeconds: number; + /** + * Give your video a custom name for easy identification. + */ + name?: string | undefined; + /** + * Output video resolution. Defaults to 480p, the lowest resolution available on your plan. + */ + resolution?: ("480p" | "720p") | undefined; + /** + * Start time of your clip (seconds). Must be ≥ 0. + */ + startSeconds?: number | undefined; + /** + * Optional style controls for replace vs animate mode and subject selection. + */ + style?: V1CharacterReplaceCreateBodyStyle | undefined; +}; + +/** + * @internal + * V1CharacterReplaceCreateBody without any key transformation, this is what + * we expect to come in as network data + */ +export type External$V1CharacterReplaceCreateBody = { + assets: External$V1CharacterReplaceCreateBodyAssets; + end_seconds: number; + name?: string | undefined; + resolution?: ("480p" | "720p") | undefined; + start_seconds?: number | undefined; + style?: External$V1CharacterReplaceCreateBodyStyle | undefined; +}; + +/** + * Takes network data, validates it, and transforms keys to match typescript object V1CharacterReplaceCreateBody + */ +const SchemaIn$V1CharacterReplaceCreateBody: z.ZodType< + V1CharacterReplaceCreateBody, // output type of this zod object + z.ZodTypeDef, + unknown +> = z + .object({ + assets: Schemas$V1CharacterReplaceCreateBodyAssets.in, + end_seconds: z.number(), + name: z.string().optional(), + resolution: z.enum(["480p", "720p"]).optional(), + start_seconds: z.number().optional(), + style: Schemas$V1CharacterReplaceCreateBodyStyle.in.optional(), + }) + .transform((obj) => { + return zodTransform(obj, { + assets: "assets", + end_seconds: "endSeconds", + name: "name", + resolution: "resolution", + start_seconds: "startSeconds", + style: "style", + }); + }); + +/** + * @internal + * Takes typescript data, validates it, and maps keys to match the expected external object External$V1CharacterReplaceCreateBody + */ +const SchemaOut$V1CharacterReplaceCreateBody: z.ZodType< + External$V1CharacterReplaceCreateBody, // output type of this zod object + z.ZodTypeDef, + V1CharacterReplaceCreateBody // the object to be transformed +> = z + .object({ + assets: Schemas$V1CharacterReplaceCreateBodyAssets.out, + endSeconds: z.number(), + name: z.string().optional(), + resolution: z.enum(["480p", "720p"]).optional(), + startSeconds: z.number().optional(), + style: Schemas$V1CharacterReplaceCreateBodyStyle.out.optional(), + }) + .transform((obj) => { + return zodTransform(obj, { + assets: "assets", + endSeconds: "end_seconds", + name: "name", + resolution: "resolution", + startSeconds: "start_seconds", + style: "style", + }); + }); + +export const Schemas$V1CharacterReplaceCreateBody = { + in: SchemaIn$V1CharacterReplaceCreateBody, + out: SchemaOut$V1CharacterReplaceCreateBody, +}; diff --git a/src/types/v1-character-replace-create-response.ts b/src/types/v1-character-replace-create-response.ts new file mode 100644 index 0000000..ffae9d9 --- /dev/null +++ b/src/types/v1-character-replace-create-response.ts @@ -0,0 +1,83 @@ +import { zodTransform } from "make-api-request-js"; +import * as z from "zod"; + +/** + * Success + */ +export type V1CharacterReplaceCreateResponse = { + /** + * The amount of credits deducted from your account to generate the video. If the status is not 'complete', this value is an estimate and may be adjusted upon completion based on the actual FPS of the output video. + * + * If video generation fails, credits will be refunded, and this field will be updated to include the refund. + */ + creditsCharged: number; + /** + * Deprecated: Previously represented the number of frames (original name of our credit system) used for video generation. Use 'credits_charged' instead. + * + * The amount of frames used to generate the video. If the status is not 'complete', the cost is an estimate and will be adjusted when the video completes. + */ + estimatedFrameCost: number; + /** + * Unique ID of the video. Use it with the [Get video Project API](https://docs.magichour.ai/api-reference/video-projects/get-video-details) to fetch status and downloads. + */ + id: string; +}; + +/** + * @internal + * V1CharacterReplaceCreateResponse without any key transformation, this is what + * we expect to come in as network data + */ +export type External$V1CharacterReplaceCreateResponse = { + credits_charged: number; + estimated_frame_cost: number; + id: string; +}; + +/** + * Takes network data, validates it, and transforms keys to match typescript object V1CharacterReplaceCreateResponse + */ +const SchemaIn$V1CharacterReplaceCreateResponse: z.ZodType< + V1CharacterReplaceCreateResponse, // output type of this zod object + z.ZodTypeDef, + unknown +> = z + .object({ + credits_charged: z.number().int(), + estimated_frame_cost: z.number().int(), + id: z.string(), + }) + .transform((obj) => { + return zodTransform(obj, { + credits_charged: "creditsCharged", + estimated_frame_cost: "estimatedFrameCost", + id: "id", + }); + }); + +/** + * @internal + * Takes typescript data, validates it, and maps keys to match the expected external object External$V1CharacterReplaceCreateResponse + */ +const SchemaOut$V1CharacterReplaceCreateResponse: z.ZodType< + External$V1CharacterReplaceCreateResponse, // output type of this zod object + z.ZodTypeDef, + V1CharacterReplaceCreateResponse // the object to be transformed +> = z + .object({ + creditsCharged: z.number().int(), + estimatedFrameCost: z.number().int(), + id: z.string(), + }) + .transform((obj) => { + return zodTransform(obj, { + creditsCharged: "credits_charged", + estimatedFrameCost: "estimated_frame_cost", + id: "id", + }); + }); + +export const Schemas$V1CharacterReplaceCreateResponse = { + in: SchemaIn$V1CharacterReplaceCreateResponse, + out: SchemaOut$V1CharacterReplaceCreateResponse, +}; diff --git a/test/v1-ai-video-editor.test.ts b/test/v1-ai-video-editor.test.ts new file mode 100644 index 0000000..f2fdd35 --- /dev/null +++ b/test/v1-ai-video-editor.test.ts @@ -0,0 +1,35 @@ +import { Client, Environment } from "magic-hour"; + +describe("tests client.v1.aiVideoEditor.create", () => { + test.concurrent( + "POST /v1/ai-video-editor | testId: success_all_params | Success test with response schema validation. Expects status code 200", + async () => { + const client = new Client({ + token: "API_TOKEN", + environment: Environment.MockServer, + }); + // Get both raw response for status and parsed response for data + const [rawResponse, response] = await Promise.all([ + client.v1.aiVideoEditor + .create({ + assets: { videoFilePath: "api-assets/id/1234.mp4" }, + endSeconds: 5.0, + name: "My Video Editor video", + startSeconds: 0.0, + style: { prompt: "Change the car color to blue" }, + }) + .asResponse(), + client.v1.aiVideoEditor.create({ + assets: { videoFilePath: "api-assets/id/1234.mp4" }, + endSeconds: 5.0, + name: "My Video Editor video", + startSeconds: 0.0, + style: { prompt: "Change the car color to blue" }, + }), + ]); + expect(rawResponse.status).toBe(200); // Exact status code match + // Response body automatically validated by Zod schema during deserialization + expect(response).toBeDefined(); + }, + ); +}); diff --git a/test/v1-character-replace.test.ts b/test/v1-character-replace.test.ts new file mode 100644 index 0000000..88f4c2c --- /dev/null +++ b/test/v1-character-replace.test.ts @@ -0,0 +1,21 @@ +import { Client, Environment } from "magic-hour"; + +describe("tests client.v1.characterReplace.create", () => { + test.concurrent( + "POST /v1/character-replace | testId: success_all_params | Success test with response schema validation. Expects status code 200", + async () => { + const client = new Client({ + token: "API_TOKEN", + environment: Environment.MockServer, + }); + // Get both raw response for status and parsed response for data + const [rawResponse, response] = await Promise.all([ + client.v1.characterReplace.create().asResponse(), + client.v1.characterReplace.create(), + ]); + expect(rawResponse.status).toBe(200); // Exact status code match + // Response body automatically validated by Zod schema during deserialization + expect(response).toBeDefined(); + }, + ); +}); From 03e9a09d95eae36cfe507fcebebf16f8f85e3bd3 Mon Sep 17 00:00:00 2001 From: David Hu Date: Wed, 15 Jul 2026 20:32:20 -0700 Subject: [PATCH 2/3] feat: add generate() to ai-video-editor and character-replace clients Adds the high-level generate workflow (upload assets, create job, poll, download) matching other video resources. Co-authored-by: Cursor --- README.md | 2 + src/resources/v1/ai-video-editor/README.md | 46 +++++++ .../v1/ai-video-editor/resource-client.ts | 104 ++++++++++++++ src/resources/v1/character-replace/README.md | 50 +++++++ .../v1/character-replace/resource-client.ts | 128 ++++++++++++++++++ 5 files changed, 330 insertions(+) diff --git a/README.md b/README.md index 22dcc45..ac828e4 100644 --- a/README.md +++ b/README.md @@ -254,6 +254,7 @@ Valid values are: `none`, `error`, `warn`, `info`, `debug` (case insensitive). I ### [v1.aiVideoEditor](src/resources/v1/ai-video-editor/README.md) - [create](src/resources/v1/ai-video-editor/README.md#create) - AI Video Editor +- [generate](src/resources/v1/ai-video-editor/README.md#generate) - AI Video Editor Generate Workflow ### [v1.aiVoiceCloner](src/resources/v1/ai-voice-cloner/README.md) @@ -294,6 +295,7 @@ Valid values are: `none`, `error`, `warn`, `info`, `debug` (case insensitive). I ### [v1.characterReplace](src/resources/v1/character-replace/README.md) - [create](src/resources/v1/character-replace/README.md#create) - Character Replace +- [generate](src/resources/v1/character-replace/README.md#generate) - Character Replace Generate Workflow ### [v1.faceDetection](src/resources/v1/face-detection/README.md) diff --git a/src/resources/v1/ai-video-editor/README.md b/src/resources/v1/ai-video-editor/README.md index f588c70..7e2f227 100644 --- a/src/resources/v1/ai-video-editor/README.md +++ b/src/resources/v1/ai-video-editor/README.md @@ -2,6 +2,52 @@ ## Module Functions + + +### AI Video Editor Generate Workflow + +The workflow performs the following action + +1. upload local assets to Magic Hour storage. So you can pass in a local path instead of having to upload files yourself +2. trigger a generation +3. poll for a completion status. This is configurable +4. if success, download the output to local directory + +> [!TIP] +> This is the recommended way to use the SDK unless you have specific needs where it is necessary to split up the actions. + +#### Parameters + +In addition to the parameters listed in the `create` section below, `generate` introduces 3 new parameters: + +- `waitForCompletion` (boolean, default true): Whether to wait for the project to complete. +- `downloadOutputs` (boolean, default true): Whether to download the generated files +- `downloadDirectory` (string, optional): Directory to save downloaded files (defaults to current directory) + +#### Example Snippet + +```typescript +import { Client } from "magic-hour"; + +const client = new Client({ token: process.env["API_TOKEN"]!! }); +const res = await client.v1.aiVideoEditor.generate( + { + assets: { videoFilePath: "/path/to/1234.mp4" }, + endSeconds: 5.0, + name: "My Video Editor video", + startSeconds: 0.0, + style: { prompt: "Change the car color to blue" }, + }, + { + waitForCompletion: true, + downloadOutputs: true, + downloadDirectory: ".", + }, +); +``` + + + ### AI Video Editor **What this API does** diff --git a/src/resources/v1/ai-video-editor/resource-client.ts b/src/resources/v1/ai-video-editor/resource-client.ts index c5fe538..bdc96f7 100644 --- a/src/resources/v1/ai-video-editor/resource-client.ts +++ b/src/resources/v1/ai-video-editor/resource-client.ts @@ -6,16 +6,120 @@ import { ResourceClientOptions, } from "make-api-request-js"; +import { + GenerateOptions, + GenerateRequestType, +} from "magic-hour/helpers/generate-type"; +import { getLogger } from "magic-hour/logger"; import * as requests from "magic-hour/resources/v1/ai-video-editor/request-types"; +import { FilesClient } from "magic-hour/resources/v1/files"; +import { VideoProjectsClient } from "magic-hour/resources/v1/video-projects"; import * as types from "magic-hour/types"; import { Schemas$V1AiVideoEditorCreateBody } from "magic-hour/types/v1-ai-video-editor-create-body"; import { Schemas$V1AiVideoEditorCreateResponse } from "magic-hour/types/v1-ai-video-editor-create-response"; +type GenerateRequest = GenerateRequestType< + requests.CreateRequest, + { + /** + * The video to edit. This value is either + * - a direct URL to the video file + * - a path to a local file + * + * Note: if the path begins with `api-assets`, it will be assumed to already be uploaded to Magic Hour's storage, and will not be uploaded again. + */ + videoFilePath: string; + } +>; + export class AiVideoEditorClient extends CoreResourceClient { constructor(coreClient: CoreClient, opts: ResourceClientOptions) { super(coreClient, opts); } + /** + * AI Video Editor + * + * Create a Video Editor video + * + * This method provides a convenient way to create a request and automatically wait for completion and download outputs. + * + * @example + * ```typescript + * import { Client } from "magic-hour"; + * + * const client = new Client({ token: process.env["API_TOKEN"]!! }); + * const res = await client.v1.aiVideoEditor.generate( + * { + * assets: { videoFilePath: "/path/to/1234.mp4" }, + * endSeconds: 5.0, + * name: "My Video Editor video", + * startSeconds: 0.0, + * style: { prompt: "Change the car color to blue" }, + * }, + * { + * waitForCompletion: true, + * downloadOutputs: true, + * downloadDirectory: ".", + * }, + * ); + * ``` + */ + async generate(request: GenerateRequest, opts: GenerateOptions = {}) { + const { + waitForCompletion = true, + downloadOutputs = true, + downloadDirectory = undefined, + ...createOpts + } = opts; + + const fileClient = new FilesClient(this._client, this._opts); + const { videoFilePath, ...restAssets } = request.assets; + + getLogger().debug( + `Uploading file ${videoFilePath} to Magic Hour's storage`, + ); + + const uploadedVideoFilePath = await fileClient.uploadFile(videoFilePath); + + getLogger().info( + `Uploaded file ${videoFilePath} to Magic Hour's storage as ${uploadedVideoFilePath}`, + ); + + const createResponse = await this.create( + { + ...request, + assets: { + ...restAssets, + videoFilePath: uploadedVideoFilePath, + }, + }, + createOpts, + ); + + getLogger().info( + `Created AiVideoEditorClient project ${createResponse.id}`, + ); + + const projectsClient = new VideoProjectsClient(this._client, this._opts); + + getLogger().debug( + `Checking result for AiVideoEditorClient project ${createResponse.id}`, + ); + + const result = await projectsClient.checkResult( + { id: createResponse.id }, + { + waitForCompletion, + downloadOutputs, + downloadDirectory, + ...createOpts, + }, + ); + + return result; + } + /** * AI Video Editor * diff --git a/src/resources/v1/character-replace/README.md b/src/resources/v1/character-replace/README.md index 1195d09..a8cdbdf 100644 --- a/src/resources/v1/character-replace/README.md +++ b/src/resources/v1/character-replace/README.md @@ -2,6 +2,56 @@ ## Module Functions + + +### Character Replace Generate Workflow + +The workflow performs the following action + +1. upload local assets to Magic Hour storage. So you can pass in a local path instead of having to upload files yourself +2. trigger a generation +3. poll for a completion status. This is configurable +4. if success, download the output to local directory + +> [!TIP] +> This is the recommended way to use the SDK unless you have specific needs where it is necessary to split up the actions. + +#### Parameters + +In addition to the parameters listed in the `create` section below, `generate` introduces 3 new parameters: + +- `waitForCompletion` (boolean, default true): Whether to wait for the project to complete. +- `downloadOutputs` (boolean, default true): Whether to download the generated files +- `downloadDirectory` (string, optional): Directory to save downloaded files (defaults to current directory) + +#### Example Snippet + +```typescript +import { Client } from "magic-hour"; + +const client = new Client({ token: process.env["API_TOKEN"]!! }); +const res = await client.v1.characterReplace.generate( + { + assets: { + imageFilePath: "/path/to/5678.png", + videoFilePath: "/path/to/1234.mp4", + }, + endSeconds: 15.0, + name: "My Character Replace video", + resolution: "720p", + startSeconds: 0.0, + style: { mode: "replace", selectionMode: "auto" }, + }, + { + waitForCompletion: true, + downloadOutputs: true, + downloadDirectory: ".", + }, +); +``` + + + ### Character Replace **What this API does** diff --git a/src/resources/v1/character-replace/resource-client.ts b/src/resources/v1/character-replace/resource-client.ts index c8396dd..7686927 100644 --- a/src/resources/v1/character-replace/resource-client.ts +++ b/src/resources/v1/character-replace/resource-client.ts @@ -6,16 +6,144 @@ import { ResourceClientOptions, } from "make-api-request-js"; +import { + GenerateOptions, + GenerateRequestType, +} from "magic-hour/helpers/generate-type"; +import { getLogger } from "magic-hour/logger"; import * as requests from "magic-hour/resources/v1/character-replace/request-types"; +import { FilesClient } from "magic-hour/resources/v1/files"; +import { VideoProjectsClient } from "magic-hour/resources/v1/video-projects"; import * as types from "magic-hour/types"; import { Schemas$V1CharacterReplaceCreateBody } from "magic-hour/types/v1-character-replace-create-body"; import { Schemas$V1CharacterReplaceCreateResponse } from "magic-hour/types/v1-character-replace-create-response"; +type GenerateRequest = GenerateRequestType< + types.V1CharacterReplaceCreateBody, + { + /** + * Reference character image used as the replacement or animation target. This value is either + * - a direct URL to the image file + * - a path to a local file + * + * Note: if the path begins with `api-assets`, it will be assumed to already be uploaded to Magic Hour's storage, and will not be uploaded again. + */ + imageFilePath: string; + /** + * Source video containing the subject to replace or animate. This value is either + * - a direct URL to the video file + * - a path to a local file + * + * Note: if the path begins with `api-assets`, it will be assumed to already be uploaded to Magic Hour's storage, and will not be uploaded again. + */ + videoFilePath: string; + } +>; + export class CharacterReplaceClient extends CoreResourceClient { constructor(coreClient: CoreClient, opts: ResourceClientOptions) { super(coreClient, opts); } + /** + * Character Replace + * + * Create a Character Replace video + * + * This method provides a convenient way to create a request and automatically wait for completion and download outputs. + * + * @example + * ```typescript + * import { Client } from "magic-hour"; + * + * const client = new Client({ token: process.env["API_TOKEN"]!! }); + * const res = await client.v1.characterReplace.generate( + * { + * assets: { + * imageFilePath: "/path/to/5678.png", + * videoFilePath: "/path/to/1234.mp4", + * }, + * endSeconds: 15.0, + * name: "My Character Replace video", + * resolution: "720p", + * startSeconds: 0.0, + * style: { mode: "replace", selectionMode: "auto" }, + * }, + * { + * waitForCompletion: true, + * downloadOutputs: true, + * downloadDirectory: ".", + * }, + * ); + * ``` + */ + async generate(request: GenerateRequest, opts: GenerateOptions = {}) { + const { + waitForCompletion = true, + downloadOutputs = true, + downloadDirectory = undefined, + ...createOpts + } = opts; + + const fileClient = new FilesClient(this._client, this._opts); + const { imageFilePath, videoFilePath, ...restAssets } = request.assets; + + getLogger().debug( + `Uploading file ${imageFilePath} to Magic Hour's storage`, + ); + getLogger().debug( + `Uploading file ${videoFilePath} to Magic Hour's storage`, + ); + + const [uploadedImageFilePath, uploadedVideoFilePath] = await Promise.all([ + fileClient.uploadFile(imageFilePath), + fileClient.uploadFile(videoFilePath), + ]); + + getLogger().info( + `Uploaded file ${imageFilePath} to Magic Hour's storage as ${uploadedImageFilePath}`, + ); + getLogger().info( + `Uploaded file ${videoFilePath} to Magic Hour's storage as ${uploadedVideoFilePath}`, + ); + + const createResponse = await this.create( + { + data: { + ...request, + assets: { + ...restAssets, + imageFilePath: uploadedImageFilePath, + videoFilePath: uploadedVideoFilePath, + }, + }, + }, + createOpts, + ); + + getLogger().info( + `Created CharacterReplaceClient project ${createResponse.id}`, + ); + + const projectsClient = new VideoProjectsClient(this._client, this._opts); + + getLogger().debug( + `Checking result for CharacterReplaceClient project ${createResponse.id}`, + ); + + const result = await projectsClient.checkResult( + { id: createResponse.id }, + { + waitForCompletion, + downloadOutputs, + downloadDirectory, + ...createOpts, + }, + ); + + return result; + } + /** * Character Replace * From 5231a7fc8f3eb664dfb39b50195a91742588b199 Mon Sep 17 00:00:00 2001 From: David Hu Date: Wed, 15 Jul 2026 20:37:07 -0700 Subject: [PATCH 3/3] fix: send character-replace create body without data wrapper Generated client incorrectly nested the request under `data` and called create with no body, which caused mock-server 422s. Co-authored-by: Cursor --- src/resources/v1/character-replace/README.md | 31 +++++--- .../v1/character-replace/request-types.ts | 73 ++++++++++++++++--- .../v1/character-replace/resource-client.ts | 20 ++--- test/v1-character-replace.test.ts | 26 ++++++- 4 files changed, 116 insertions(+), 34 deletions(-) diff --git a/src/resources/v1/character-replace/README.md b/src/resources/v1/character-replace/README.md index a8cdbdf..fc2c302 100644 --- a/src/resources/v1/character-replace/README.md +++ b/src/resources/v1/character-replace/README.md @@ -84,15 +84,16 @@ For detailed examples, see the [product page](https://magichour.ai/products/char #### Parameters -| Parameter | Required | Description | Example | -| ----------------- | :------: | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `data` | ✗ | | `{"assets": {"imageFilePath": "api-assets/id/5678.png", "videoFilePath": "api-assets/id/1234.mp4"}, "endSeconds": 15.0, "name": "My Character Replace video", "resolution": "720p", "startSeconds": 0.0}` | -| `└─ assets` | ✓ | Source video and reference character image for the job. | `{"imageFilePath": "api-assets/id/5678.png", "videoFilePath": "api-assets/id/1234.mp4"}` | -| `└─ endSeconds` | ✓ | End time of your clip (seconds). Must be greater than start_seconds. | `15.0` | -| `└─ name` | ✗ | Give your video a custom name for easy identification. | `"My Character Replace video"` | -| `└─ resolution` | ✗ | Output video resolution. Defaults to 480p, the lowest resolution available on your plan. | `"720p"` | -| `└─ startSeconds` | ✗ | Start time of your clip (seconds). Must be ≥ 0. | `0.0` | -| `└─ style` | ✗ | Optional style controls for replace vs animate mode and subject selection. | `{"mode": "replace", "selectionMode": "auto"}` | +| Parameter | Required | Description | Example | +| ------------------ | :------: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | +| `assets` | ✓ | Source video and reference character image for the job. | `{"imageFilePath": "api-assets/id/5678.png", "videoFilePath": "api-assets/id/1234.mp4"}` | +| `└─ imageFilePath` | ✓ | Reference character image used as the replacement or animation target. This value is either - a direct URL to the video file - `file_path` field from the response of the [upload urls API](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls). See the [file upload guide](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls#input-file) for details. | `"api-assets/id/5678.png"` | +| `└─ videoFilePath` | ✓ | Source video containing the subject to replace or animate. This value is either - a direct URL to the video file - `file_path` field from the response of the [upload urls API](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls). See the [file upload guide](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls#input-file) for details. | `"api-assets/id/1234.mp4"` | +| `endSeconds` | ✓ | End time of your clip (seconds). Must be greater than start_seconds. | `15.0` | +| `name` | ✗ | Give your video a custom name for easy identification. | `"My Character Replace video"` | +| `resolution` | ✗ | Output video resolution. Defaults to 480p, the lowest resolution available on your plan. | `"720p"` | +| `startSeconds` | ✗ | Start time of your clip (seconds). Must be ≥ 0. | `0.0` | +| `style` | ✗ | Optional style controls for replace vs animate mode and subject selection. | `{"mode": "replace", "selectionMode": "auto"}` | #### Example Snippet @@ -100,7 +101,17 @@ For detailed examples, see the [product page](https://magichour.ai/products/char import { Client } from "magic-hour"; const client = new Client({ token: process.env["API_TOKEN"]!! }); -const res = await client.v1.characterReplace.create(); +const res = await client.v1.characterReplace.create({ + assets: { + imageFilePath: "api-assets/id/5678.png", + videoFilePath: "api-assets/id/1234.mp4", + }, + endSeconds: 15.0, + name: "My Character Replace video", + resolution: "720p", + startSeconds: 0.0, + style: { mode: "replace", selectionMode: "auto" }, +}); ``` #### Response diff --git a/src/resources/v1/character-replace/request-types.ts b/src/resources/v1/character-replace/request-types.ts index ee085b0..deab5b0 100644 --- a/src/resources/v1/character-replace/request-types.ts +++ b/src/resources/v1/character-replace/request-types.ts @@ -2,16 +2,44 @@ import { zodTransform } from "make-api-request-js"; import * as z from "zod"; import { - External$V1CharacterReplaceCreateBody, - Schemas$V1CharacterReplaceCreateBody, - V1CharacterReplaceCreateBody, -} from "magic-hour/types/v1-character-replace-create-body"; + External$V1CharacterReplaceCreateBodyAssets, + Schemas$V1CharacterReplaceCreateBodyAssets, + V1CharacterReplaceCreateBodyAssets, +} from "magic-hour/types/v1-character-replace-create-body-assets"; +import { + External$V1CharacterReplaceCreateBodyStyle, + Schemas$V1CharacterReplaceCreateBodyStyle, + V1CharacterReplaceCreateBodyStyle, +} from "magic-hour/types/v1-character-replace-create-body-style"; /** * CreateRequest */ export type CreateRequest = { - data?: V1CharacterReplaceCreateBody | undefined; + /** + * Source video and reference character image for the job. + */ + assets: V1CharacterReplaceCreateBodyAssets; + /** + * End time of your clip (seconds). Must be greater than start_seconds. + */ + endSeconds: number; + /** + * Give your video a custom name for easy identification. + */ + name?: string | undefined; + /** + * Output video resolution. Defaults to 480p, the lowest resolution available on your plan. + */ + resolution?: ("480p" | "720p") | undefined; + /** + * Start time of your clip (seconds). Must be ≥ 0. + */ + startSeconds?: number | undefined; + /** + * Optional style controls for replace vs animate mode and subject selection. + */ + style?: V1CharacterReplaceCreateBodyStyle | undefined; }; /** @@ -20,7 +48,12 @@ export type CreateRequest = { * we expect to come in as network data */ export type External$CreateRequest = { - data?: External$V1CharacterReplaceCreateBody | undefined; + assets: External$V1CharacterReplaceCreateBodyAssets; + end_seconds: number; + name?: string | undefined; + resolution?: ("480p" | "720p") | undefined; + start_seconds?: number | undefined; + style?: External$V1CharacterReplaceCreateBodyStyle | undefined; }; /** @@ -32,11 +65,21 @@ const SchemaIn$CreateRequest: z.ZodType< unknown > = z .object({ - data: Schemas$V1CharacterReplaceCreateBody.in.optional(), + assets: Schemas$V1CharacterReplaceCreateBodyAssets.in, + end_seconds: z.number(), + name: z.string().optional(), + resolution: z.enum(["480p", "720p"]).optional(), + start_seconds: z.number().optional(), + style: Schemas$V1CharacterReplaceCreateBodyStyle.in.optional(), }) .transform((obj) => { return zodTransform(obj, { - data: "data", + assets: "assets", + end_seconds: "endSeconds", + name: "name", + resolution: "resolution", + start_seconds: "startSeconds", + style: "style", }); }); @@ -50,11 +93,21 @@ const SchemaOut$CreateRequest: z.ZodType< CreateRequest // the object to be transformed > = z .object({ - data: Schemas$V1CharacterReplaceCreateBody.out.optional(), + assets: Schemas$V1CharacterReplaceCreateBodyAssets.out, + endSeconds: z.number(), + name: z.string().optional(), + resolution: z.enum(["480p", "720p"]).optional(), + startSeconds: z.number().optional(), + style: Schemas$V1CharacterReplaceCreateBodyStyle.out.optional(), }) .transform((obj) => { return zodTransform(obj, { - data: "data", + assets: "assets", + endSeconds: "end_seconds", + name: "name", + resolution: "resolution", + startSeconds: "start_seconds", + style: "style", }); }); diff --git a/src/resources/v1/character-replace/resource-client.ts b/src/resources/v1/character-replace/resource-client.ts index 7686927..b6d08d6 100644 --- a/src/resources/v1/character-replace/resource-client.ts +++ b/src/resources/v1/character-replace/resource-client.ts @@ -19,7 +19,7 @@ import { Schemas$V1CharacterReplaceCreateBody } from "magic-hour/types/v1-charac import { Schemas$V1CharacterReplaceCreateResponse } from "magic-hour/types/v1-character-replace-create-response"; type GenerateRequest = GenerateRequestType< - types.V1CharacterReplaceCreateBody, + requests.CreateRequest, { /** * Reference character image used as the replacement or animation target. This value is either @@ -109,13 +109,11 @@ export class CharacterReplaceClient extends CoreResourceClient { const createResponse = await this.create( { - data: { - ...request, - assets: { - ...restAssets, - imageFilePath: uploadedImageFilePath, - videoFilePath: uploadedVideoFilePath, - }, + ...request, + assets: { + ...restAssets, + imageFilePath: uploadedImageFilePath, + videoFilePath: uploadedVideoFilePath, }, }, createOpts, @@ -173,7 +171,7 @@ export class CharacterReplaceClient extends CoreResourceClient { * POST /v1/character-replace */ create( - request: requests.CreateRequest = {}, + request: requests.CreateRequest, opts?: RequestOptions, ): ApiPromise { return this._client.makeRequest({ @@ -181,9 +179,7 @@ export class CharacterReplaceClient extends CoreResourceClient { path: "/v1/character-replace", auth: ["bearerAuth"], contentType: "application/json", - body: request.data - ? Schemas$V1CharacterReplaceCreateBody.out.parse(request.data) - : undefined, + body: Schemas$V1CharacterReplaceCreateBody.out.parse(request), responseSchema: Schemas$V1CharacterReplaceCreateResponse.in, opts, }); diff --git a/test/v1-character-replace.test.ts b/test/v1-character-replace.test.ts index 88f4c2c..967d61e 100644 --- a/test/v1-character-replace.test.ts +++ b/test/v1-character-replace.test.ts @@ -10,8 +10,30 @@ describe("tests client.v1.characterReplace.create", () => { }); // Get both raw response for status and parsed response for data const [rawResponse, response] = await Promise.all([ - client.v1.characterReplace.create().asResponse(), - client.v1.characterReplace.create(), + client.v1.characterReplace + .create({ + assets: { + imageFilePath: "api-assets/id/5678.png", + videoFilePath: "api-assets/id/1234.mp4", + }, + endSeconds: 15.0, + name: "My Character Replace video", + resolution: "720p", + startSeconds: 0.0, + style: { mode: "replace", selectionMode: "auto" }, + }) + .asResponse(), + client.v1.characterReplace.create({ + assets: { + imageFilePath: "api-assets/id/5678.png", + videoFilePath: "api-assets/id/1234.mp4", + }, + endSeconds: 15.0, + name: "My Character Replace video", + resolution: "720p", + startSeconds: 0.0, + style: { mode: "replace", selectionMode: "auto" }, + }), ]); expect(rawResponse.status).toBe(200); // Exact status code match // Response body automatically validated by Zod schema during deserialization