From 3d3ba911b3e68d5f9f011be24a748db5dad855f0 Mon Sep 17 00:00:00 2001 From: David Hu Date: Wed, 27 May 2026 10:36:17 -0700 Subject: [PATCH] feat: add generate() to audio-to-video client Adds the high-level generate workflow (upload assets, create job, poll, download) matching other video resources like lip-sync and image-to-video. Co-authored-by: Cursor --- README.md | 1 + src/resources/v1/audio-to-video/README.md | 49 +++++++ .../v1/audio-to-video/resource-client.ts | 135 +++++++++++++++++- 3 files changed, 184 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3a0bd59..ba2d77e 100644 --- a/README.md +++ b/README.md @@ -274,6 +274,7 @@ Valid values are: `none`, `error`, `warn`, `info`, `debug` (case insensitive). I ### [v1.audioToVideo](src/resources/v1/audio-to-video/README.md) +- [generate](src/resources/v1/audio-to-video/README.md#generate) - Audio To Video Generate Workflow - [create](src/resources/v1/audio-to-video/README.md#create) - Audio-to-Video ### [v1.autoSubtitleGenerator](src/resources/v1/auto-subtitle-generator/README.md) diff --git a/src/resources/v1/audio-to-video/README.md b/src/resources/v1/audio-to-video/README.md index f0c6cf9..41b2b6e 100644 --- a/src/resources/v1/audio-to-video/README.md +++ b/src/resources/v1/audio-to-video/README.md @@ -2,6 +2,55 @@ ## Module Functions + + +### Audio To Video 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.audioToVideo.generate( + { + assets: { + audioFilePath: "/path/to/1234.mp3", + imageFilePath: "/path/to/1234.png", + }, + endSeconds: 15.0, + name: "Audio To Video video", + resolution: "720p", + startSeconds: 0.0, + }, + { + waitForCompletion: true, + downloadOutputs: true, + downloadDirectory: ".", + }, +); +``` + + + ### Audio-to-Video **What this API does** diff --git a/src/resources/v1/audio-to-video/resource-client.ts b/src/resources/v1/audio-to-video/resource-client.ts index d7a1109..fa3b3c1 100644 --- a/src/resources/v1/audio-to-video/resource-client.ts +++ b/src/resources/v1/audio-to-video/resource-client.ts @@ -6,16 +6,149 @@ import { ResourceClientOptions, } from "make-api-request-js"; +import { types } from "magic-hour"; +import { + GenerateOptions, + GenerateRequestType, +} from "magic-hour/helpers/generate-type"; +import { getLogger } from "magic-hour/logger"; +import { FilesClient } from "magic-hour/resources/v1/files"; import * as requests from "magic-hour/resources/v1/audio-to-video/request-types"; -import * as types from "magic-hour/types"; +import { VideoProjectsClient } from "magic-hour/resources/v1/video-projects"; import { Schemas$V1AudioToVideoCreateBody } from "magic-hour/types/v1-audio-to-video-create-body"; import { Schemas$V1AudioToVideoCreateResponse } from "magic-hour/types/v1-audio-to-video-create-response"; +type GenerateRequest = GenerateRequestType< + requests.CreateRequest, + { + /** + * The path of the audio file. This value is either + * - a direct URL to the audio 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. + */ + audioFilePath: string; + /** + * Reference image for the initial frame of the video. 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 | undefined; + } +>; + export class AudioToVideoClient extends CoreResourceClient { constructor(coreClient: CoreClient, opts: ResourceClientOptions) { super(coreClient, opts); } + /** + * Audio-to-Video + * + * Create a Audio To Video 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.audioToVideo.generate( + * { + * assets: { + * audioFilePath: "/path/to/1234.mp3", + * imageFilePath: "/path/to/1234.png", + * }, + * endSeconds: 15.0, + * name: "Audio To Video video", + * resolution: "720p", + * startSeconds: 0.0, + * }, + * { + * 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 { audioFilePath, imageFilePath, ...restAssets } = request.assets; + + getLogger().debug( + `Uploading file ${audioFilePath} to Magic Hour's storage`, + ); + if (imageFilePath) { + getLogger().debug( + `Uploading file ${imageFilePath} to Magic Hour's storage`, + ); + } + + const [uploadedAudioFilePath, uploadedImageFilePath] = await Promise.all([ + fileClient.uploadFile(audioFilePath), + imageFilePath + ? fileClient.uploadFile(imageFilePath) + : Promise.resolve(imageFilePath), + ]); + + getLogger().info( + `Uploaded file ${audioFilePath} to Magic Hour's storage as ${uploadedAudioFilePath}`, + ); + if (imageFilePath) { + getLogger().info( + `Uploaded file ${imageFilePath} to Magic Hour's storage as ${uploadedImageFilePath}`, + ); + } + + const createResponse = await this.create( + { + ...request, + assets: { + ...restAssets, + audioFilePath: uploadedAudioFilePath, + imageFilePath: imageFilePath + ? uploadedImageFilePath + : imageFilePath, + }, + }, + createOpts, + ); + + getLogger().info( + `Created AudioToVideoClient project ${createResponse.id}`, + ); + + const projectsClient = new VideoProjectsClient(this._client, this._opts); + + getLogger().debug( + `Checking result for AudioToVideoClient project ${createResponse.id}`, + ); + + const result = await projectsClient.checkResult( + { id: createResponse.id }, + { + waitForCompletion, + downloadOutputs, + downloadDirectory, + ...createOpts, + }, + ); + + return result; + } + /** * Audio-to-Video *