メディアとアップロード
Nodaro の SDK を使って、TypeScript からファイルをアップロードし、メディアライブラリを一覧表示し、SNS の動画をダウンロードし、クリップをトリミングし、字幕を焼き込み、画像と動画を合成します。
client.uploads は、ファイルを Nodaro に保存します。client.library は、すでに持っているメディアを一覧表示します。client.media は、メディアの準備と編集を行います。SNS の動画のダウンロード、クリップのトリミング、字幕の焼き込み、画像と動画の合成です。client.media のほとんどのメソッドはジョブを開始し、その jobId を返します。これは client.jobs.getStatus() でポーリングします。これらのメソッドは、アップロードと音声とメディアの REST API のエンドポイントを呼び出します。
メソッド
| メソッド | 内容 |
|---|---|
uploads.upload(file) | ファイルを 1 つアップロードし、その公開 URL を取得します |
library.list(params?) | 保存されているメディアを一覧表示します |
media.downloadVideo(input) | SNS の動画を、自分のストレージにダウンロードします |
media.downloadVideoProgress(downloadId, opts?) | ダウンロードの進行状況を追跡します |
media.videoMetadata(input) | SNS の動画の長さとサイズを、ダウンロードせずに読み取ります |
media.saveToStorage(input) | メディアの URL を、自分のストレージにコピーします |
media.process(input) | 保存済みのファイルを、無料でカットまたはクロップします |
media.trimVideo(input) | 動画を範囲でトリミングします |
media.trimAudio(input) | オーディオをトリミングするか、動画から抽出します |
media.addCaptions(input) | 動画に字幕を焼き込みます |
media.stillToVideo(input) | 画像とオーディオトラックを、動画にします |
media.slideshow(input) | 画像と任意のオーディオを、スライドショーにします |
media.videoOverlay(input) | タイミング付きの画像レイヤーを、動画の上に配置します |
media.imageCollage(input) | 2〜30 枚の画像を、1 つのコラージュに結合します |
media.imageOverlay(input) | 画像、テキスト、QR、図形のレイヤーを、画像の上に配置します |
media.suggestOverlayPlacement(input) | レイヤーをどこに置くべきか、ビジョンモデルに尋ねます |
client.uploads
uploads.upload(file)
ファイルを 1 つアップロードし(POST /v1/upload、マルチパート)、その公開 URL と保存の詳細を返します。SDK はファイルをフォームデータとして送信し、マルチパートの境界はランタイムに設定させます。
upload(file: File): Promise<UploadResult>Prop
Type
const result = await client.uploads.upload(file)
const clip = await client.nodes.runAndWait("generate-video", {
prompt: "The camera slowly pushes in",
imageUrl: result.url,
})| フィールド | 型 | 説明 |
|---|---|---|
url | string | 保存されたファイルの公開 URL です。imageUrl、videoUrl、audioUrl として渡してください。 |
assetId | string | null | 保存された項目の ID です。匿名でのアップロードでは null です。 |
thumbnailUrl | string | null | 画像と動画のサムネイルです。ない場合は null です。 |
category | string | image、video、audio のいずれかです。 |
filename | string | 表示用のファイル名です。 |
mimeType | string | サーバーが判定したメディアタイプです。ブラウザーが宣言したものと異なることがあります。たとえば、application/octet-stream として送られた .mp4 ファイルは video/mp4 になります。 |
sizeBytes | number | 保存されたサイズです。 |
r2Key | string | ファイルのストレージキーです。 |
ストレージが満杯の場合は、StorageExceededError をスローします。画像アップロード(Upload Image)、動画アップロード(Upload Video)、オーディオアップロード(Upload Audio)の各ノードも、同じストレージを使います。
client.library
library.list(params?)
保存されているメディアを、カーソル付きで一覧表示します(GET /v1/library)。デフォルトでは、自分のライブラリに保存した項目と、自分に共有された項目が、エディターのメディアピッカーと同じ形で返されます。
list(params?: ListLibraryParams): Promise<{ data: LibraryAsset[]; nextCursor: string | null; totalCount?: number }>Prop
Type
const { data: videos, nextCursor } = await client.library.list({ type: "video", limit: 20 })
for (const asset of videos) console.log(asset.filename, asset.url)各 LibraryAsset は、id、type、filename、mimeType、sizeBytes、url、thumbnailUrl、metadata、isLibraryItem、uploadSource、source、sourceDetail、createdAt を持ちます。totalCount があるのは、最初のページだけです。
client.media:インポートと準備
media.downloadVideo(input)
YouTube、TikTok、Instagram、X、Facebook の動画を、自分のストレージにダウンロードします(POST /v1/download-video)。返されるのは、ジョブ ID ではなく downloadId です。続けて downloadVideoProgress() を使ってください。完成したファイルは、ライブラリに入ります。
downloadVideo(input: {
url: string
maxHeight?: number
sectionStartSec?: number
sectionEndSec?: number
requireAudio?: boolean
}): Promise<{ downloadId: string }>Prop
Type
const { downloadId } = await client.media.downloadVideo({
url: "https://youtu.be/dQw4w9WgXcQ",
maxHeight: 720,
})media.downloadVideoProgress(downloadId, opts?)
ダウンロードのライブの進行状況を、非同期イテレーターとしてストリーミングします(GET /v1/download-video/progress/:id、サーバー送信イベント)。ダウンロードが完了または失敗するまで、約 500 ミリ秒ごとにイベントを 1 つ生成し、その後終了します。
downloadVideoProgress(downloadId: string, opts?: { signal?: AbortSignal }): AsyncGenerator<DownloadVideoProgress>Prop
Type
for await (const event of client.media.downloadVideoProgress(downloadId)) {
console.log(`${event.phase} ${event.percent}%`)
if (event.phase === "completed") console.log("Stored at", event.videoUrl)
if (event.phase === "failed") console.error(event.error)
}各イベントは { phase, percent, videoUrl?, thumbnailUrl?, error? } です。downloadVideo() が返ったら、すぐにイテレートを始めてください。進行状況のレコードは、ダウンロードが終わってからほどなく期限切れになります。クライアントの timeoutMs は適用されません。大きなダウンロードには数分かかるためです。
media.videoMetadata(input)
SNS の動画の長さ、サイズ、タイトル、ライブ配信かどうかを、ダウンロードせずに読み取ります(POST /v1/video-metadata)。ジョブを介さず、直接答えを返します。一部の区間だけを取得するかどうかを判断するために使います。
videoMetadata(input: { url: string }): Promise<VideoMetadata>Prop
Type
const meta = await client.media.videoMetadata({ url: "https://youtu.be/dQw4w9WgXcQ" })各フィールドはベストエフォートです。プラットフォームによっては、すべてを報告しないことがあります。
media.saveToStorage(input)
メディアファイルを、URL から自分の Nodaro のストレージにコピーします(POST /v1/save-to-storage)。ファイルを取得するのはサーバーなので、クライアントを経由しません。
saveToStorage(input: { mediaUrl: string; filename?: string; mediaType?: "image" | "video" | "audio" }): Promise<{ jobId: string }>Prop
Type
const { jobId } = await client.media.saveToStorage({ mediaUrl: "https://example.com/intro.mp4", mediaType: "video" })ストレージに保存(Save to Storage)ノードも、ワークフローの中で同じことを行います。
media.process(input)
すでに自分のストレージにあるファイルをカットまたはクロップします(POST /v1/media/process)。直接答えを返し、料金はかかりません。有料のステップの前に、素材を準備するために使います。
process(input: {
sourceUrl: string
type: "video" | "audio"
trim?: { startTime: number; endTime: number }
crop?: { x: number; y: number; width: number; height: number }
format?: "mp4" | "webm" | "mp3" | "wav" | "m4a" | "aac"
deleteSource?: boolean
}): Promise<{ data: MediaProcessResult }>Prop
Type
const { data } = await client.media.process({
sourceUrl: stored.url,
type: "video",
trim: { startTime: 12, endTime: 42 },
})
console.log(data.url, data.sizeBytes)結果には、url、thumbnailUrl、assetId、sizeBytes、mimeType、metadata が含まれます。
media.trimVideo(input)
動画を範囲でトリミングします(POST /v1/trim-video)。動画のトリミング(Trim Video)ノードと同じです。範囲は、都合のよい単位で指定できます。
trimVideo(input: {
videoUrl: string
startTime?: number
endTime?: number
trimStartFrames?: number
trimEndFrames?: number
trimStartSeconds?: number
trimEndSeconds?: number
keepFirstSeconds?: number
keepLastSeconds?: number
}): Promise<{ jobId: string }>Prop
Type
const { jobId } = await client.media.trimVideo({ videoUrl, keepFirstSeconds: 15 })media.trimAudio(input)
オーディオを範囲でトリミングするか、動画から抽出します(POST /v1/trim-audio)。オーディオのトリミング(Trim Audio)ノードと同じです。
trimAudio(input: {
videoUrl?: string
audioUrl?: string
audioFormat?: "mp3" | "wav" | "aac"
startTime?: number
endTime?: number
}): Promise<{ jobId: string }>Prop
Type
const { jobId } = await client.media.trimAudio({ videoUrl, startTime: 5, endTime: 35 })client.media:字幕
media.addCaptions(input)
動画に字幕を焼き込みます(POST /v1/add-captions)。字幕を追加(Add Captions)ノードと同じです。単語は text で指定するか、単語ごとにタイミングの付いた captions を指定するか、Nodaro に音声を文字起こしさせます(これがデフォルトです)。
addCaptions(input: AddCaptionsInput): Promise<{ jobId: string }>Prop
Type
const { jobId } = await client.media.addCaptions({
videoUrl: "https://example.com/talk.mp4",
style: "word-highlight",
maxWordsPerLine: 2,
})単語がどこから来るか。呼び出しに複数の情報源がある場合、単語ごとにタイミングの付いた captions が優先され、次に subtitle スタイルの text、その次に文字起こしの順です。subtitle では、text がそのまま字幕になります。動画全体で 1 つの静止したブロックとして焼き込まれ、文字起こしの結果に置き換わることはありません。改行を強制するには \n を使います。キネティックスタイルでは、text はフォールバックにすぎません。文字起こしで何も見つからなかった場合や autoTranscribe が false の場合に使われ、その単語は動画全体に均等に配置されます。
スタイルとルック。キネティックスタイルでは、look を指定しない場合、outline としてレンダリングされます。subtitle では clean としてレンダリングされます。明示的なレバーは、ルックの個々のフィールドを上書きします。独自の look を指定したセグメントは、そのプリセットを起点とし、トップレベルのレバーは継承しません。
subtitle でのレバー。スタイル用のレバー(look、fontFamily、fontWeight、strokeColor、strokeWidth、uppercase、positionY、maxWordsPerLine)は、subtitle でも機能します。これらのいずれかを使った subtitle は、キネティックと同じ料金になり、プレーンテキストの subtitle は、より低い料金になります。highlightColor と animate はキネティック専用で、subtitle では 400 で拒否されます。自動で文字起こしされた subtitle も、キネティックと同じ料金になります。
行。word-highlight、karaoke、bouncy は、1 行ずつ表示します。行が終わるのは、文の終わり、0.5 秒以上の間、フレーム幅の約 85% を占めたとき、または maxWordsPerLine の単語数に達したときです。word-pop は 1 単語ずつ表示するため、maxWordsPerLine はここでは影響しません。tiktok-words のページは、文の終わりや間を越えることはありません。どちらも、最後に発話された単語の後、最大 1.5 秒間画面に残ります。単語の startMs と endMs は、その効果が現れるタイミングを示すものであり、表示され続ける長さではありません。
文字起こしエンジン。キネティックスタイルには単語ごとのタイミングが必要なため、transcribeProvider は、ここでのデフォルトである incredibly-fast-whisper、または elevenlabs-stt のいずれかである必要があります。whisper には単語ごとのタイミングがありません。文字起こしが単語の唯一の情報源になる場合に限り、400 validation_error で拒否されます。subtitle では、フレーズ単位のタイミングしか必要ないため、どのエンジンでも使えます。
フレームレート。スタイル付きのレンダリングは、元の動画のフレームレートを保ち、15〜60 の整数に丸めます。フレームレートが可変の動画や、フレーム数の上限を超える長さのクリップは、30 fps でレンダリングされます。
文字起こしして修正し、それから焼き込む
client.audio.transcribe() のジョブが持つ words は、captions が取る形と完全に同じです。単語を修正し、2 回目の文字起こしをせずに字幕を焼き込みます。
import type { TranscribeJobOutput } from "@nodaro/sdk"
const { jobId } = await client.audio.transcribe({
audioUrl: "https://example.com/talk.mp3",
provider: "elevenlabs-stt", // always returns word timings
})
// ...poll until the job completes, then:
const { data: job } = await client.jobs.get(jobId)
const { words = [] } = job.output_data as TranscribeJobOutput
const corrected = words.map((w, i) => (i === 7 ? { ...w, text: "Nodaro" } : w))
await client.media.addCaptions({
videoUrl: "https://example.com/talk.mp4",
captions: corrected,
autoTranscribe: false,
style: "word-highlight",
})client.media:レンダリングと合成
media.stillToVideo(input)
1 枚の静止画と 1 つのオーディオトラックを、MP4 にします(POST /v1/still-to-video)。静止画から動画(Still to Video)ノードと同じです。AI モデルを使わずにサーバー上でレンダリングされ、クレジットはかかりません。動画の長さはオーディオと同じで、長さを指定するフィールドはありません。
stillToVideo(input: {
imageUrl: string
audioUrl: string
motion?: "none" | "zoom-in" | "zoom-out" | "pan-left" | "pan-right" | "ken-burns"
intensity?: number
resolution?: "720p" | "1080p" | "4K"
aspectRatio?: "16:9" | "9:16" | "1:1" | "4:3"
fps?: 24 | 30
fit?: "cover" | "contain"
padColor?: string
}): Promise<{ jobId: string }>Prop
Type
const { jobId } = await client.media.stillToVideo({ imageUrl: coverUrl, audioUrl: podcastUrl, motion: "ken-burns" })media.slideshow(input)
2〜100 枚の画像と任意のオーディオトラックを、MP4 のスライドショーにします(POST /v1/slideshow)。スライドショー(Slideshow)ノードと同じです。クレジットはかかりません。画像が 1 枚だけの場合は、stillToVideo() を使ってください。
slideshow(input: {
imageUrls: string[]
audioUrl?: string
imageDurations?: Array<number | null>
perImageDuration?: number
transition?: string
transitionDuration?: number
motion?: "none" | "zoom-in" | "zoom-out" | "ken-burns" | "alternate"
intensity?: number
resolution?: "720p" | "1080p" | "4K"
aspectRatio?: "16:9" | "9:16" | "1:1" | "4:3"
fps?: 24 | 30
fit?: "cover" | "contain"
padColor?: string
}): Promise<{ jobId: string }>Prop
Type
const { jobId } = await client.media.slideshow({ imageUrls: frames, audioUrl: musicUrl, motion: "alternate" })オーディオがある場合、imageDurations で一部の画像の時間を固定しない限り、時間は均等に分割されます。オーディオがない場合、動画の長さは画像の枚数に perImageDuration を掛けた値になり、無音になります。
media.videoOverlay(input)
タイミング付きの画像レイヤーを 1〜20 個、1 回の処理で動画の上に配置します(POST /v1/video-overlay)。動画オーバーレイ(Video Overlay)ノードと同じです。レイヤーの数や長さにかかわらず、1 回の実行につき 20 クレジット かかります。動画自体のオーディオは、そのまま保たれます。
videoOverlay(input: VideoOverlayRequest): Promise<{ jobId: string }>Prop
Type
各レイヤーは、imageUrl と、0〜3,600 の秒数で指定する start を取り、任意で end を取ります。end を指定しない場合は、動画が終わるまで表示されます。preset は card、corner-badge、full-frame のいずれかです。ボックスを指定するフィールドの x と y は -100〜100、width と height は 1〜100 で、いずれも出力フレームに対する割合(パーセント)です。ボックスのフィールドを明示すると、プリセットより優先されます。どちらも指定しないレイヤーは、右下、または指定した corner の位置に付くコーナーバッジになります。opacity は 0〜1、animate はデフォルトでオンです。zIndex は 0〜100 です。
const { jobId } = await client.media.videoOverlay({
videoUrl,
layers: [
{ imageUrl: logoUrl, start: 0, preset: "corner-badge", corner: "top-right" },
{ imageUrl: offerCardUrl, start: 8, end: 14, preset: "card" },
],
})完了したジョブの出力には、videoUrl、thumbnailUrl、width、height、durationSec、warnings が含まれます。各警告は { layer?, slot?, code, detail } の形式で、code は clipped、skipped、animated_first_frame、audio_reencoded などです。
media.imageCollage(input)
2〜30 枚の画像を、1 枚の大きな 2K または 4K のコラージュに結合します(POST /v1/image-collage)。画像コラージュ(Image Collage)ノードと同じです。
imageCollage(input: {
imageUrls: string[]
imageSizes?: Array<0 | 1 | 2 | 3>
numbered?: boolean
imageLabels?: Array<string | null>
badgePosition?: "top-left" | "top-right"
layout?: "smart" | "grid"
resolution?: "2K" | "4K"
aspectRatio?: string
gap?: number
backgroundColor?: string
}): Promise<{ jobId: string }>Prop
Type
const { jobId } = await client.media.imageCollage({
imageUrls: shotUrls,
numbered: true,
imageLabels: ["Wide", "Medium", "Close-up"],
})番号とラベルは、レイアウト、出力サイズ、料金を変えることはありません。画像に対して長すぎるラベルは、省略記号で短縮されます。
media.imageOverlay(input)
1〜12 個のレイヤーを、ベース画像の上にピクセル単位で正確に配置します(POST /v1/image-overlay)。画像オーバーレイ(Image Overlay)ノードと同じです。AI モデルを使わない合成です。10 クレジット に加えて、variants に追加するプラットフォームサイズごとに 2 クレジット かかります。
imageOverlay(input: {
imageUrl: string
layers: Array<{
kind?: "image" | "text" | "qr" | "shape"
imageUrl?: string
anchor?: "top-left" | "top" | "top-right" | "left" | "center" | "right" | "bottom-left" | "bottom" | "bottom-right"
x?: number
y?: number
width?: number
height?: number
opacity?: number
rotation?: number
blend?: "over" | "multiply" | "screen"
fit?: "contain" | "cover" | "stretch"
shadow?: { blur: number; offsetX: number; offsetY: number; color: string; opacity: number }
roundedCorners?: number
zIndex?: number
text?: OverlayTextStyle
qr?: OverlayQrStyle
shape?: OverlayShapeStyle
effects?: OverlayImageEffects
}>
canvas?: { width: number; height: number; backgroundColor?: string }
baseFit?: "contain" | "cover"
outputFormat?: "png" | "jpg" | "webp"
variants?: string[]
maskMode?: "none" | "layers" | "around" | "outside"
maskSpread?: number
qrText?: string
}): Promise<{ jobId: string }>Prop
Type
レイヤー。レイヤーは、次の 4 種類のいずれかです。
- 画像:デフォルトの
kind: "image"とimageUrlです。 - 本物のテキスト:
kind: "text"とtextオブジェクトです。内容、フォント、太さ、色、配置、輪郭、背景ボックス、ベースの高さに対する割合(パーセント)でのサイズを設定します。 - QR コード:
kind: "qr"とqr: { text }です。 - 単色の図形:
kind: "shape"とshape: { shape, color }です。
位置は、ベース画像に対する割合(パーセント)です。そのため、同じ呼び出しが 1K のプレビューにも 4K のレンダリングにも通用します。
anchorは 9 つの位置のいずれかで、デフォルトはcenterです。xとyは、アンカーからのレイヤーの移動量で、ベースの幅と高さに対する割合(パーセント)です。右または下のアンカーでは、負の値を指定すると内側に移動します。widthはベースの幅に対する割合(パーセント)で、デフォルトは 25 です。高さは、heightを設定しない限りレイヤーのアスペクト比に従い、設定した場合はfitがレイヤーをボックスにどう収めるかを決めます。opacityは 0〜1、rotationはレイヤーの中心を軸にした度数、blendはover(デフォルト)、multiply、screenのいずれかです。shadowはやわらかい影を追加し、roundedCornersは角をピクセル単位で丸め、zIndexは重なりの順序を決めます。
const { jobId } = await client.media.imageOverlay({
imageUrl: productShotUrl,
layers: [{ imageUrl: logoUrl, anchor: "bottom-right", x: -4, y: -6, width: 12, opacity: 0.95 }],
variants: ["youtube-thumbnail"],
})完了したジョブの出力には、imageUrl、その width と height、maskUrl、そして variants(それぞれ { id, label, width, height, url })が含まれます。SVG のレイヤーは、対象のサイズで鮮明に描画されます。qr.fromInput: true を指定した QR レイヤーは、そのペイロードを qrText から取得します。qrText を指定しない実行は、それを示す 400 で拒否されます。
media.suggestOverlayPlacement(input)
1 つのレイヤーをベース画像のどこに置くべきかを、ビジョンモデルに尋ねます(POST /v1/image-overlay/suggest-placement)。モデルは、レイヤーを顔、メインの被写体、複雑なテクスチャから避けて配置します。ポーリングするジョブを介さず直接答えを返し、画像の説明(Describe Image)の呼び出し 1 回として課金されます。何も合成されません。配置を適用するのはあなたです。
suggestOverlayPlacement(input: {
imageUrl: string
layerAspect?: number
intent?: string
safeArea?: { x: number; y: number; w: number; h: number }
llmModel?: string
}): Promise<{ jobId: string; placement: OverlayPlacement }>Prop
Type
const { placement } = await client.media.suggestOverlayPlacement({
imageUrl: baseUrl,
intent: "a logo",
layerAspect: 2.5,
})
const { reason, ...box } = placement // anchor, x, y and width, in imageOverlay's units
await client.media.imageOverlay({ imageUrl: baseUrl, layers: [{ imageUrl: logoUrl, ...box }] })placement は、imageOverlay のパーセント単位での anchor、x、y、width に加えて、ユーザーに表示できる 1 文の reason を持ちます。jobId は、課金の記録です。
よくある質問
関連ページ
ボイスとオーディオ
ジョブと実行
アップロード
音声とメディア
字幕を追加
最終更新