Nodaro ドキュメント
ドキュメントノードリファレンスモデルAI エージェント(MCP)開発者向けセルフホスティングリサーチ
TypeScript SDK

メディアとアップロード

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,
})
フィールド型説明
urlstring保存されたファイルの公開 URL です。imageUrl、videoUrl、audioUrl として渡してください。
assetIdstring | null保存された項目の ID です。匿名でのアップロードでは null です。
thumbnailUrlstring | null画像と動画のサムネイルです。ない場合は null です。
categorystringimage、video、audio のいずれかです。
filenamestring表示用のファイル名です。
mimeTypestringサーバーが判定したメディアタイプです。ブラウザーが宣言したものと異なることがあります。たとえば、application/octet-stream として送られた .mp4 ファイルは video/mp4 になります。
sizeBytesnumber保存されたサイズです。
r2Keystringファイルのストレージキーです。

ストレージが満杯の場合は、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 は、課金の記録です。

よくある質問

最終更新

目次