מדיה והעלאות
העלאת קבצים, ספריית המדיה, ייבוא סרטונים מקישורים, קיצוץ קליפים, צריבת כתוביות ושילוב שכבות של תמונות וסרטונים עם ה-SDK של Nodaro.
client.uploads שומר את הקבצים שלכם ב-Nodaro, client.library מחזיר את המדיה שכבר יש לכם, והמשאב client.gallery מאפשר לעיין בגלריה הציבורית ובמועדפים שלכם. המשאב client.media מכין ועורך מדיה: הוא מוריד סרטונים מקישורים, מקצץ קליפים, צורב כתוביות ומשלב שכבות של תמונות וסרטונים. רוב המתודות של client.media מתחילות משימה ומחזירות את ה-jobId שלה, ואת הסטטוס של המשימה בודקים מדי כמה שניות עם client.jobs.getStatus(). המתודות קוראות לנקודות הקצה שבדפי ה-API העלאות וקול ומדיה.
מתודות
| מתודה | מה היא עושה |
|---|---|
uploads.upload(file) | העלאת קובץ אחד וקבלת הכתובת הציבורית שלו |
uploads.uploadJson(json) | שמירת מסמך JSON וקבלת הכתובת שלו |
uploads.importUrl(url, options?) | העתקת תמונה מכתובת אינטרנט לאחסון שלכם |
library.list(params?) | רשימת המדיה השמורה שלכם וחיפוש בה |
gallery.browse(params?) | עיון בגלריה הציבורית |
gallery.toggleFavorite(jobId) | סימון יצירה כמועדפת, או הסרת הסימון |
gallery.favorites() | רשימת המזהים של המשימות שסימנתם כמועדפות |
contentRights.declaration() | קריאת הצהרת זכויות התוכן |
contentRights.confirm(input) | אישור ההצהרה לקישור אחד לפני הייבוא שלו |
contentRights.authorization() | קריאת ההרשאה הקבועה לייבוא דרך מפתחות וסוכנים |
contentRights.authorize(input) | מתן הרשאה לייבוא דרך מפתחות וסוכנים, מתוך הסשן של בעל החשבון עצמו |
contentRights.revokeAuthorization() | ביטול ההרשאה הזו |
media.downloadVideo(input) | הורדת הסרטון שמאחורי קישור ושמירה שלו באחסון שלכם |
media.downloadVideoProgress(downloadId, opts?) | קבלת ההתקדמות של הורדה בזמן אמת |
media.videoMetadata(input) | קריאת האורך והגודל של סרטון מרשת חברתית בלי להוריד אותו |
media.saveToStorage(input) | העתקת קובץ מדיה מכתובת URL לאחסון שלכם |
media.process(input) | קיצוץ או חיתוך של קובץ שמור, בחינם |
media.discard(input) | מחיקת קובץ שיצרתם זה עתה ושכבר אין לכם צורך בו |
media.trimVideo(input) | קיצוץ סרטון לטווח מסוים |
media.trimAudio(input) | קיצוץ אודיו, או חילוץ שלו מסרטון |
media.splitMedia(input) | חיתוך סרטון או קובץ אודיו לחלקים שהאורך שלהם בין מינימום למקסימום, או חיתוך סרטון לחלק אחד לכל שוט |
media.addCaptions(input) | צריבת כתוביות בסרטון |
media.stillToVideo(input) | הפיכת תמונה ורצועת אודיו לסרטון |
media.slideshow(input) | הפיכת תמונות, ואודיו אופציונלי, למצגת |
media.videoOverlay(input) | הצבת שכבות תמונה מתוזמנות מעל סרטון |
media.imageCollage(input) | סידור של 2–30 תמונות בקולאז׳ אחד |
media.imageOverlay(input) | הצבת שכבות של תמונה, טקסט, קוד QR וצורה על תמונה |
media.videoMask(input) | מעקב אחרי נושא שמתואר בטקסט וקבלת סרטון מסכה |
media.maskComposite(input) | הצבת סרטון אחד מעל סרטון אחר דרך סרטון מסכה, או פיקסול או טשטוש של האזור הממוסך |
media.suggestOverlayPlacement(input) | בקשה ממודל ראייה להציע איפה למקם שכבה |
client.uploads
uploads.upload(file)
מעלה קובץ אחד (POST /v1/upload, בפורמט multipart) ומחזירה את הכתובת הציבורית שלו ואת פרטי האחסון. ה-SDK שולח את הקובץ כ-form data, ומשאיר לסביבת הריצה לקבוע את ה-boundary של ה-multipart.
upload(file: File): Promise<UploadResult>מאפיין
סוג
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 | הכתובת הציבורית של הקובץ השמור. העבירו אותה בתור imageUrl, videoUrl או audioUrl. |
assetId | string | null | המזהה של הפריט השמור, או null בהעלאה אנונימית. |
thumbnailUrl | string | null | תמונה ממוזערת לתמונות ולווידאו, או null. |
category | string | image, video או audio. |
filename | string | שם התצוגה של הקובץ. |
mimeType | string | סוג המדיה שהשרת קבע. הוא יכול להיות שונה מהסוג שהדפדפן הצהיר עליו, למשל video/mp4 לקובץ .mp4 שנשלח כ-application/octet-stream. |
sizeBytes | number | הגודל של הקובץ השמור. |
r2Key | string | המפתח של הקובץ באחסון. |
המתודה זורקת StorageExceededError כשנפח האחסון שלכם מלא. הרכיבים העלאת תמונה, העלאת וידאו והעלאת אודיו משתמשים באותו אחסון.
uploads.uploadJson(json)
שומרת מסמך JSON של עד 25 MB (POST /v1/upload-json) ומחזירה את { url }, הכתובת הציבורית שלו. אפשר לשמור כך, למשל, את מצב העיצוב של עורך תמונות: מעבירים את הכתובת שלו ל-jobs.fromUpload בתור editStateUrl, כדי שאפשר יהיה לפתוח את העריכה שוב.
uploadJson(json: Record<string, unknown> | readonly unknown[]): Promise<UploadJsonResult>קשה לנחש את הכתובת, אבל היא ציבורית, ולכן לעולם אל תשמרו סוד במסמך כזה. המתודה לא יוצרת פריט בספרייה.
uploads.importUrl(url, options?)
מעתיקה תמונה מכתובת אינטרנט לאחסון שלכם ומחזירה אותה (POST /v1/media/import-url, עם גוף הבקשה { url, confirmationId? }). העותק השמור נחשב להעלאה שלכם, והוא מוגש כך שאלמנט <canvas> בדפדפן יכול לקרוא את הפיקסלים שלו, למשל כדי לחתוך אותו. תמונה באתר אחר לא מאפשרת את זה.
importUrl(url: string, options?: { confirmationId?: string }): Promise<ImportUrlResult>מאפיין
סוג
הפלטפורמה שומרת את הקובץ רק אם אפשר לפענח אותו כתמונה: JPEG, PNG, WebP או GIF. תמונת HEIC, HEIF או AVIF מומרת ל-JPEG או ל-PNG.
try {
const picture = await client.uploads.importUrl("https://example.org/photo.jpg")
console.log(picture.url, picture.width, picture.height)
} catch (err) {
if (err instanceof StorageExceededError && err.reason === "file_too_large") {
// one picture over 20 MB, not a full storage
}
}| שדה | סוג | תיאור |
|---|---|---|
url | string | הכתובת הציבורית של התמונה השמורה. |
thumbnailUrl | string | null | תמונה ממוזערת, או null כשאי אפשר היה ליצור אותה. |
assetId | string | null | המזהה של הפריט השמור, או null כשלא נכתב פריט. |
mimeType | string | סוג המדיה הסופי. מקור HEIC, HEIF או AVIF נשמר כ-JPEG או כ-PNG. |
sizeBytes | number | הגודל של הקובץ השמור. |
filename | string | החלק האחרון בנתיב של הכתובת, או imported-<id>.<ext>. |
width | number | הרוחב בפיקסלים, או 0 כשאי אפשר היה לקרוא אותו. |
height | number | הגובה בפיקסלים, או 0 כשאי אפשר היה לקרוא אותו. |
| סטטוס | שגיאה | מתי |
|---|---|---|
| 400 | NodaroError עם הקוד validation_error | הכתובת היא לא http או https, או שהיא כתובת IP פרטית או שמורה. הקובץ הוא לא תמונה שאפשר לפענח, או שהפורמט שלו לא מתקבל, כגון SVG או TIFF. |
| 422 | NodaroError עם הקוד fetch_failed | אי אפשר היה להגיע לכתובת, או שהיא החזירה שגיאה. גם שם מארח שמפנה לכתובת פרטית נכלל במקרה הזה. |
| 403 | ImportRefusedError, שהיא גם ForbiddenError | ייבוא קישורים חסום בחשבון שלכם, או שהחשבון סגור, לפי מדיניות המפרים החוזרים של הפריסה (imports_blocked); הפריסה לא מאפשרת ייבוא מהאתר (import_source_disabled); או שאישור זכויות התוכן חסר כשהוא נדרש, שייך לקישור אחר או לחשבון אחר, לא עדכני, שתוקפו פג או שכבר נוצל (קוד rights_confirmation_*). |
| 413 | StorageExceededError עם reason בערך file_too_large | התמונה גדולה מ-20 MB. |
| 413 | StorageExceededError עם reason בערך storage_limit_exceeded | נפח האחסון שלכם מלא. ב-details נמצאים usedBytes, quotaBytes, remainingBytes ו-tier. |
רכיב העלאת תמונה משתמש באותו ייבוא כשמדביקים קישור לתמונה. כדי למחוק תמונה מיובאת שכבר אין לכם צורך בה, קראו ל-media.discard().
client.library
library.list(params?)
מחזירה את המדיה השמורה שלכם בעמודים, עם cursor (GET /v1/library). כברירת מחדל היא מחזירה את הפריטים ששמרתם בספרייה שלכם, ובנוסף פריטים ששותפו איתכם, כמו שחלון בחירת המדיה בעורך מציג אותם.
list(params?: ListLibraryParams): Promise<{ data: LibraryAsset[]; nextCursor: string | null; totalCount?: number }>מאפיין
סוג
const { data: videos, nextCursor } = await client.library.list({ type: "video", limit: 20 })
for (const asset of videos) console.log(asset.filename, asset.url)const { data, nextCursor } = await client.library.list({ type: "image", q: "poster" })כל LibraryAsset כולל את id, type, filename, mimeType, sizeBytes, url, thumbnailUrl, metadata, isLibraryItem, uploadSource, source, sourceDetail, jobId ו-createdAt. השדה jobId הוא המשימה שיצרה את הקובץ, או null בהעלאה. שרתים מגרסאות ישנות יותר משמיטים אותו. השדה totalCount מופיע רק בעמוד הראשון.
client.gallery
הגלריה הציבורית של יצירות שהושלמו, והמועדפים שלכם. מועדף הוא סימון של יצירה: יצירה שלכם, או יצירה ציבורית של כל אחד אחר. במשימות שלכם הסימון מופיע בשדה Job.favorited, והמתודה client.jobs.list({ favorited: true }) מחזירה את המשימות המועדפות. בפריסה שמסתירה את הגלריה, הנתיבים האלה מחזירים 404.
gallery.browse(params?)
מחזירה עמוד אחד מהגלריה הציבורית, מהחדש לישן (GET /v1/gallery). לא צריך אסימון.
browse(params?: BrowseGalleryParams): Promise<GalleryPage>מאפיין
סוג
const { data: items, nextCursor } = await client.gallery.browse({ type: "image", limit: 20 })העמוד הוא { data: GalleryItem[], nextCursor, totalCount? }, והשדה totalCount מופיע רק בעמוד הראשון. כל פריט הוא { id, type, jobName, outputUrl, thumbnailUrl, createdAt, prompt, model, preview? }, והשדה id הוא מזהה המשימה. הפרמטר favoritesOnly עובד רק עם ה-userId שלכם, מ-client.me(). עם favoritesOnly ובלי אסימון, המתודה זורקת UnauthorizedError. עם userId של אדם אחר, או בלי userId, היא זורקת ForbiddenError.
gallery.toggleFavorite(jobId)
מסמנת את המשימה כמועדפת, או מסירה את הסימון כשהמשימה כבר מועדפת (POST /v1/gallery/favorite). השדה favorited מציין את המצב של המשימה אחרי הקריאה.
toggleFavorite(jobId: string): Promise<{ favorited: boolean }>const { favorited } = await client.gallery.toggleFavorite(jobId)אפשר לסמן כמועדפות את המשימות שלכם ואת המשימות הציבוריות. על משימה פרטית של אדם אחר, המתודה זורקת NotFoundError. אפליקציות OAuth צריכות את ההרשאה assets:write.
gallery.favorites()
מחזירה את המזהים של המשימות שסימנתם כמועדפות, מהחדש לישן (GET /v1/gallery/favorites). אפליקציות OAuth צריכות את ההרשאה assets:read או jobs:read.
favorites(): Promise<string[]>const ids = await client.gallery.favorites()client.contentRights
המשאב client.contentRights קורא את ההצהרה שמי שמייבא קישור מאשר לפני הייבוא, ורושם את האישור שמצורף אחר כך לייבוא (/v1/content-rights). הכללים מפורטים בדף ה-API זכויות תוכן.
contentRights.declaration()
declaration(): Promise<{ version: number; text: string }>מחזירה את ההצהרה הנוכחית (GET /v1/content-rights/declaration). הציגו את text כמו שהוא, עם תיבת סימון שמתחילה לא מסומנת. אישור מציין את ה-version. הקריאה לא דורשת פרטי גישה.
contentRights.confirm(input)
confirm(input: { url: string; declarationVersion: number }): Promise<{
id: string
declarationVersion: number
expiresAt: string
}>רושמת שהחשבון המחובר אישר את ההצהרה לקישור אחד (POST /v1/content-rights/confirm). אפשר לאשר כל קישור http(s) שייבוא מקבל: דף של סרטון, קובץ, תמונה או דף ששירות הדיבוב קורא. העבירו את ה-id שחזר בתור confirmationId ל-media.downloadVideo(), ל-uploads.importUrl() או לייבוא שמושך את הקישור, כגון voices.dub() ו-audio.transcribe(). אישור אחד פותח הורדה אחת של הקישור הזה, מאותו חשבון, תחת ההצהרה הנוכחית ובתוך שעה. תשובת 409 rights_confirmation_stale פירושה שההצהרה השתנתה אחרי שקיבלתם אותה: טענו אותה שוב ובקשו את האישור שוב.
const { version, text } = await client.contentRights.declaration()
// Show `text` with a checkbox. Once it is ticked:
const { id } = await client.contentRights.confirm({ url, declarationVersion: version })
const { downloadId } = await client.media.downloadVideo({ url, maxHeight: 1080, confirmationId: id })הגדרה של הפריסה קובעת אם הורדה בלי אישור נדחית. כשההגדרה מופעלת, הורדה כזו נדחית עם 403 rights_confirmation_required. תמיד שלחו אישור.
contentRights.authorization()
authorization(): Promise<{ id: string; declarationVersion: number; createdAt: string } | null>מחזירה את ההרשאה הקבועה של החשבון לייבוא דרך מפתחות ה-API, האפליקציות המחוברות וסוכני ה-AI שלו (GET /v1/content-rights/authorization). כשאין הרשאה כזו, המתודה מחזירה null: ההרשאה לא ניתנה מעולם, בוטלה או ניתנה להצהרה ישנה יותר.
contentRights.authorize(input)
authorize(input: { declarationVersion: number }): Promise<{
id: string
declarationVersion: number
createdAt: string
}>רושמת שבעל החשבון נתן הרשאה לייבוא דרך המפתחות, האפליקציות והסוכנים שלו, לגרסת ההצהרה שהוא ראה (POST /v1/content-rights/authorization). קראו לה מתוך הסשן המחובר של בעל החשבון עצמו. קריאה דרך מפתח, אפליקציה או סוכן נדחית עם 403 forbidden. תשובת 409 rights_confirmation_stale פירושה שההצהרה השתנתה: טענו אותה שוב ובקשו את ההרשאה שוב.
contentRights.revokeAuthorization()
revokeAuthorization(): Promise<{ revoked: boolean }>מבטלת את ההרשאה הקבועה (DELETE /v1/content-rights/authorization). הרשומה נשמרת. השדה revoked הוא false כשלא היה מה לבטל. קראו לה מתוך הסשן של בעל החשבון עצמו.
client.media: ייבוא והכנה
media.downloadVideo(input)
מורידה לאחסון שלכם את הסרטון שמאחורי קישור: הדף של הסרטון או קישור ישיר לקובץ וידאו (POST /v1/download-video). המתודה מחזירה downloadId, ולא מזהה משימה. את ההתקדמות מקבלים עם downloadVideoProgress(). הקובץ המוגמר נשמר בספרייה שלכם.
downloadVideo(input: {
url: string
maxHeight?: number
sectionStartSec?: number
sectionEndSec?: number
exactSection?: boolean
requireAudio?: boolean
confirmationId?: string
}): Promise<{ downloadId: string }>מאפיין
סוג
const { downloadId } = await client.media.downloadVideo({
url: "https://youtu.be/dQw4w9WgXcQ",
maxHeight: 720,
})media.downloadVideoProgress(downloadId, opts?)
מזרימה את ההתקדמות של הורדה בזמן אמת, כאיטרטור אסינכרוני שמקבל אירועי שרת (SSE) מ-GET /v1/download-video/progress/:id. היא מחזירה אירוע בערך כל 500 אלפיות שנייה, עד שההורדה מסתיימת או נכשלת, ואז האיטרטור מסתיים.
downloadVideoProgress(downloadId: string, opts?: { signal?: AbortSignal }): AsyncGenerator<DownloadVideoProgress>מאפיין
סוג
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)
מחזירה את האורך, הממדים והכותרת של סרטון מרשת חברתית, וגם אם הוא שידור חי, בלי להוריד אותו (POST /v1/video-metadata). התשובה מגיעה ישירות, בלי משימה. השתמשו בה כדי להחליט אם להוריד רק חלק מהסרטון.
videoMetadata(input: { url: string }): Promise<VideoMetadata>מאפיין
סוג
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 }>מאפיין
סוג
const { jobId } = await client.media.saveToStorage({ mediaUrl: "https://example.com/intro.mp4", mediaType: "video" })רכיב שמירה לאחסון עושה את אותו הדבר בתוך תהליך.
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
}, options?: { timeoutMs?: number; signal?: AbortSignal }): Promise<{ data: MediaProcessResult }>מאפיין
סוג
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.discard(input)
מוחקת קובץ שיצרתם זה עתה ושכבר אין לכם צורך בו (POST /v1/media/discard). למשל, הורדה מקישור שהחלפתם לפני שהשתמשתם בה, קטע מקוצץ שהוחלף בקטע חדש יותר ותמונה שייבאתם ואז ביטלתם.
discard(input: { url: string }): Promise<{ data: { discarded: boolean } }>מאפיין
סוג
המתודה discard() לא מבצעת מחיקה כללית. הקובץ נמחק רק כשכל התנאים האלה מתקיימים:
- הקובץ הוא שלכם.
- הקובץ נוצר ב-
downloadVideo(), ב-process()או בייבוא מכתובת URL, כגוןuploads.importUrl(). - הקובץ נוצר לפני פחות מ-24 שעות.
- אף משימה שלכם לא משתמשת בקובץ, לא כפלט ולא כקלט, כגון ה-
videoUrlשל הרצה.
במקרה כזה התשובה היא { data: { discarded: true } }. אחרת שום דבר לא משתנה, והקריאה נכשלת עם שגיאת 409 שהקוד שלה הוא not_discardable. הערך discarded: false פירושו שבכתובת הזו לא היה קובץ שלכם: הקובץ כבר נמחק, או שהוא לא שלכם.
await client.media.discard({ url: oldCut.url })media.trimVideo(input)
מקצצת סרטון לטווח מסוים (POST /v1/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 }>מאפיין
סוג
const { jobId } = await client.media.trimVideo({ videoUrl, keepFirstSeconds: 15 })media.trimAudio(input)
מקצצת אודיו לטווח מסוים, או מחלצת אותו מסרטון (POST /v1/trim-audio), כמו רכיב קיצוץ אודיו.
trimAudio(input: {
videoUrl?: string
audioUrl?: string
audioFormat?: "mp3" | "wav" | "aac"
startTime?: number
endTime?: number
}): Promise<{ jobId: string }>מאפיין
סוג
const { jobId } = await client.media.trimAudio({ videoUrl, startTime: 5, endTime: 35 })media.splitMedia(input)
חותכת סרטון, קובץ אודיו או את שניהם לחלקים רצופים (POST /v1/split-media), כמו רכיב פיצול למקטעים. כל קלט מפוצל בנפרד.
splitMedia(input:
| {
videoUrl?: string
audioUrl?: string
mode?: "duration"
chunkDuration: number
minPartDuration?: number
audioFormat?: "mp3" | "wav" | "aac"
}
| { videoUrl: string; mode: "shots" }
): Promise<{ jobId: string }>מאפיין
סוג
הרצה עולה 22 קרדיטים, לא משנה כמה חלקים נוצרים. בפלט של המשימה המוגמרת נמצאים videoUrls ו-audioUrls (החלקים לפי הסדר), chunkCount ו-partDurationsSec (האורך המדויק של כל חלק). כששני הקלטים פוצלו, השדה audioPartDurationsSec מפרט את האורכים של חלקי האודיו. קובץ יכול להיות באורך של עד 3 שעות, ולהתפצל ל-200 חלקים לכל היותר.
const { jobId } = await client.media.splitMedia({ videoUrl, chunkDuration: 60, minPartDuration: 30 })עם mode: "shots", הסרטון נחתך במקום זאת לחלק אחד לכל שוט, בנקודות החיתוך שנמדדות מהקובץ. המצב הזה לא מקבל אורך חלק, ומקבל רק סרטון: שליחה של audioUrl נפרד נדחית עם 400. כל חלק שומר על האודיו של הסרטון עצמו. סרטונים יכולים להיות באורך של עד 30 דקות, עם 200 שוטים לכל היותר, באותו מחיר. הפלט כולל אז גם את shotCount ואת shots, שכל פריט בו הוא { index, startSec, endSec, durationSec }.
const { jobId } = await client.media.splitMedia({ videoUrl: "https://example.com/ad.mp4", mode: "shots" })client.media: כתוביות
media.addCaptions(input)
צורבת כתוביות בסרטון (POST /v1/add-captions), כמו רכיב הוספת כתוביות. אפשר לשלוח את המילים ב-text, לשלוח captions מתוזמנות לפי מילה, או לתת ל-Nodaro לתמלל את הדיבור, וזו ברירת המחדל.
addCaptions(input: AddCaptionsInput): Promise<{ jobId: string }>מאפיין
סוג
const { jobId } = await client.media.addCaptions({
videoUrl: "https://example.com/talk.mp4",
style: "word-highlight",
maxWordsPerLine: 2,
})מאיפה המילים מגיעות. כשבקריאה יש כמה מקורות, captions המתוזמנות לפי מילה גוברות, אחריהן text בסגנון subtitle, ורק אחר כך התמלול. בסגנון subtitle, השדה text הוא הכתובית: הוא נצרב כגוש סטטי אחד לכל אורך הסרטון, ותמלול אף פעם לא מחליף אותו. כתבו בו \n כדי לכפות מעבר שורה. בסגנון מונפש, השדה text הוא רק חלופה. הוא משמש כשהתמלול לא מוצא כלום או כש-autoTranscribe הוא false, ואז המילים שלו מתחלקות באופן שווה לאורך הסרטון.
סגנונות ומראות. בסגנונות המונפשים, כש-look לא מוגדר, הכתוביות מרונדרות כ-outline. בסגנון subtitle, הן מרונדרות כ-clean. אפשרויות עיצוב שמגדירים במפורש גוברות על שדות בודדים של המראה. מקטע שמציין look משלו מתחיל מהערכה הזו, ולא יורש את אפשרויות העיצוב שברמה העליונה.
אפשרויות עיצוב בסגנון subtitle. אפשרויות העיצוב (look, fontFamily, fontWeight, strokeColor, strokeWidth, uppercase, positionY ו-maxWordsPerLine) עובדות גם בסגנון subtitle. על כתובית עם אחת מהן משלמים את המחיר של סגנון מונפש, ועל כתובית של טקסט פשוט משלמים את המחיר הנמוך יותר. האפשרויות highlightColor ו-animate קיימות רק בסגנונות מונפשים, ובסגנון subtitle הן נדחות עם 400. גם על כתובית שנוצרה מתמלול אוטומטי משלמים את המחיר של סגנון מונפש.
שורות. הסגנונות word-highlight, karaoke ו-bouncy מציגים שורה אחת בכל פעם. שורה מסתיימת בסוף משפט, בהפסקה של 0.5 שנייה או יותר, כשהיא ממלאת כ-85% מרוחב הפריים או כשהיא מגיעה ל-maxWordsPerLine מילים. הסגנון word-pop מציג מילה אחת בכל פעם, ולכן ל-maxWordsPerLine אין בו השפעה. עמוד של tiktok-words אף פעם לא חוצה סוף משפט או הפסקה. גם השורות וגם העמודים נשארים על המסך 1.5 שניות לכל היותר אחרי המילה האחרונה שנאמרה. השדות startMs ו-endMs של מילה מתזמנים את האפקט שלה, לא את משך הזמן שהיא נשארת על המסך.
מנועי תמלול. סגנון מונפש צריך תזמון לכל מילה, ולכן הערך של transcribeProvider חייב להיות incredibly-fast-whisper, ברירת המחדל כאן, או elevenlabs-stt. למנוע whisper אין תזמון לכל מילה. הוא נדחה עם 400 validation_error רק כשהתמלול הוא המקור האפשרי היחיד של המילים. בסגנון subtitle כל מנוע עובד, כי כתובית צריכה רק תזמון ברמת המשפט.
קצב פריימים. רינדור מעוצב שומר על קצב הפריימים של סרטון המקור, מעוגל למספר שלם בין 15 ל-60. מקור עם קצב פריימים משתנה, או קליפ ארוך מדי למגבלת הפריימים, מרונדר ב-30 פריימים לשנייה.
תמלול, תיקון ואז צריבה
לשדה words של משימת client.audio.transcribe() יש בדיוק המבנה שהשדה captions מקבל. תקנו מילה, ואז צרבו את הכתוביות בלי תמלול נוסף:
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)
הופכת תמונת סטילס אחת ורצועת אודיו אחת לקובץ MP4 (POST /v1/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 }>מאפיין
סוג
const { jobId } = await client.media.stillToVideo({ imageUrl: coverUrl, audioUrl: podcastUrl, motion: "ken-burns" })media.slideshow(input)
הופכת 2–100 תמונות ורצועת אודיו אופציונלית למצגת בקובץ MP4 (POST /v1/slideshow), כמו רכיב מצגת. הקריאה לא עולה קרדיטים. לתמונה אחת, קראו ל-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 }>מאפיין
סוג
const { jobId } = await client.media.slideshow({ imageUrls: frames, audioUrl: musicUrl, motion: "alternate" })עם אודיו, הזמן מתחלק באופן שווה, אלא אם השדה imageDurations קובע אורך קבוע לחלק מהתמונות. בלי אודיו, אורך הסרטון הוא מספר התמונות כפול perImageDuration, ואין בסרטון אודיו.
media.videoOverlay(input)
מציבה 1–20 שכבות תמונה מתוזמנות מעל סרטון במעבר אחד (POST /v1/video-overlay), כמו רכיב שכבה על וידאו. הרצה עולה 22 קרדיטים, לא משנה כמה שכבות יש ומה האורך. האודיו של הסרטון עצמו נשמר כמו שהוא.
videoOverlay(input: VideoOverlayRequest): Promise<{ jobId: string }>מאפיין
סוג
כל שכבה מקבלת imageUrl ו-start, בשניות מ-0 עד 3,600, ו-end אופציונלי. בלי end, השכבה נשארת עד סוף הסרטון. הערך של preset הוא card, corner-badge או full-frame. שדות התיבה x ו-y נעים מ--100 עד 100, והשדות width ו-height מ-1 עד 100, כולם באחוזים מפריים הפלט. שדה תיבה שמוגדר במפורש גובר על preset. שכבה בלי אף אחד מהם היא תג פינה, למטה מימין או בפינה שמצוינת ב-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 }, עם קוד כגון clipped, skipped, animated_first_frame או audio_reencoded.
media.imageCollage(input)
מסדרת 2–30 תמונות בקולאז׳ אחד גדול, ב-2K או ב-4K (POST /v1/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 }>מאפיין
סוג
const { jobId } = await client.media.imageCollage({
imageUrls: shotUrls,
numbered: true,
imageLabels: ["Wide", "Medium", "Close-up"],
})מספרים וכיתובים אף פעם לא משנים את הפריסה, את גודל הפלט או את המחיר. כיתוב ארוך מדי לתמונה שלו מקוצר, עם שלוש נקודות בסופו.
media.imageOverlay(input)
מציבה 1–12 שכבות על תמונת בסיס, בדיוק ברמת הפיקסל (POST /v1/image-overlay), כמו רכיב שכבת תמונה. המתודה משלבת שכבות בלי מודל AI. הקריאה עולה 11 קרדיטים, וכל גודל נוסף של פלטפורמה ב-variants מוסיף למחיר.
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 }>מאפיין
סוג
שכבות. לכל שכבה יש אחד מארבעה סוגים:
- תמונה: הערך
kind: "image", ברירת המחדל, עםimageUrl. - טקסט אמיתי: הערך
kind: "text"עם אובייקטtext. האובייקט קובע את התוכן, הגופן, העובי, הצבע, היישור, קו המתאר ותיבת הרקע, וגם גודל באחוזים מגובה הבסיס. - קוד QR: הערך
kind: "qr"עםqr: { text }. - צורה שטוחה: הערך
kind: "shape"עםshape: { shape, color }.
המיקומים הם באחוזים מתמונת הבסיס, ולכן אותה קריאה עובדת על תצוגה מקדימה ב-1K ועל רינדור ב-4K:
- השדה
anchorהוא אחד מתשעה מיקומים,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 עם qr.fromInput: true לוקחת את התוכן שלה מ-qrText, והרצה בלי qrText נדחית עם שגיאת 400 שמציינת אותו.
media.videoMask(input)
עוקבת אחרי נושא שמתואר בטקסט פשוט לאורך סרטון, ומחזירה סרטון מסכה בשחור־לבן, שבו הנושא לבן (POST /v1/video-mask), כמו רכיב מסכת וידאו.
videoMask(input: {
videoUrl: string
prompt: string
threshold?: number
provider?: "sam-3.1" | "sam-3"
holdLastMask?: boolean
minSubjectHeightPct?: number
maxSubjectHeightPct?: number
zone?: { leftPct?: number; rightPct?: number; topPct?: number; bottomPct?: number } | null
perShot?: boolean
redetectEverySec?: number
}): Promise<{ jobId: string }>מאפיין
סוג
המחיר נקבע לפי אורך הקליפ: ב-SAM 3.1 הוא נע מ-55 עד 3,108 קרדיטים. קליפ בקצב של יותר מ-30 פריימים לשנייה נספר כארוך יותר, באופן יחסי. בפלט של המשימה המוגמרת נמצאים videoUrl (המסכה), width, height, frames, chunks, shots, coverage, kept ו-recipe. השדה chunks סופר את הבקשות שנשלחו למודל. השדה shots הוא 1, אלא אם נמצאו חיתוכים במצב perShot. השדה recipe הוא הגרסה של שיטת המיסוך, והוא משתנה בכל פעם שאותו קליפ עם אותן הגדרות היה נותן מסכה אחרת. השדה kept הוא false רק כשמסנני הגודל והאזור לא השאירו כלום באף פריים. בדקו את kept ולא את coverage, שמעוגל ל-4 ספרות אחרי הנקודה.
media.maskComposite(input)
מציבה סרטון חזית מעל סרטון רקע דרך סרטון מסכה (POST /v1/mask-composite), כמו רכיב שילוב לפי מסכה. לבן במסכה מציג את החזית. עם fill: "pixelate" או "blur", האזור הממוסך מציג את הרקע עצמו, מפוקסל או מטושטש, ולא צריך חזית.
maskComposite(input: {
foregroundUrl?: string
backgroundUrl: string
maskUrl: string
invertMask?: boolean
feather?: number
grow?: number
opacity?: number
fill?: "video" | "pixelate" | "blur"
pixelSizePct?: number
blurStrengthPct?: number
backgroundFit?: "cover" | "contain" | "stretch"
audioSource?: "foreground" | "background" | "mix" | "none"
}): Promise<{ jobId: string }>מאפיין
סוג
הרצה עולה 11 קרדיטים לכל 5 שניות של התוצאה, או חלק מהן.
const { jobId: maskJobId } = await client.media.videoMask({ videoUrl: talkUrl, prompt: "the person" })
// Poll the mask job. Its output videoUrl is the mask.
const { jobId } = await client.media.maskComposite({ foregroundUrl: talkUrl, backgroundUrl: beachUrl, maskUrl })media.suggestOverlayPlacement(input)
שואלת מודל ראייה איפה למקם שכבה אחת על תמונת בסיס (POST /v1/image-overlay/suggest-placement). המודל מרחיק את השכבה מפנים, מהנושא המרכזי ומטקסטורות עמוסות. התשובה מגיעה ישירות, בלי משימה שצריך לבדוק את הסטטוס שלה, והקריאה מתומחרת כמו קריאה אחת של רכיב תיאור תמונה. שום שכבה לא משולבת בתמונה: את המיקום מחילים בעצמכם.
suggestOverlayPlacement(input: {
imageUrl: string
layerAspect?: number
intent?: string
safeArea?: { x: number; y: number; w: number; h: number }
llmModel?: string
}): Promise<{ jobId: string; placement: OverlayPlacement }>מאפיין
סוג
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 נמצאים anchor, x, y ו-width, ביחידות האחוזים של imageOverlay, ובנוסף reason, משפט אחד שאפשר להציג למשתמש. השדה jobId הוא רשומת החיוב.
שאלות נפוצות
איך מעלים קובץ עם ה-SDK של Nodaro?
קראו ל-client.uploads.upload(file) עם אובייקט File. המתודה מחזירה כתובת ציבורית, ואפשר להעביר אותה לכל רכיב שמקבל כתובת של תמונה, של סרטון או של אודיו.
איך מוסיפים כתוביות לסרטון מקוד?
קראו ל-client.media.addCaptions עם videoUrl ועם style. כשאין text ואין captions, Nodaro מתמלל את הדיבור. הקריאה מחזירה jobId, ובמשימה המוגמרת נמצא הסרטון עם הכתוביות.
אילו מתודות מדיה לא עולות קרדיטים?
המתודות stillToVideo ו-slideshow מרנדרות בלי מודל AI ולא עולות קרדיטים, וגם המתודה media.process לא עולה כלום. המתודה videoOverlay עולה 22 קרדיטים להרצה, והמתודה imageOverlay עולה 11 קרדיטים, וכל גודל נוסף של פלטפורמה מוסיף למחיר שלה.
איך מסמנים יצירה כמועדפת מקוד?
קראו ל-client.gallery.toggleFavorite(jobId). המתודה מסמנת את המשימה כמועדפת, או מסירה את הסימון כשהמשימה כבר מועדפת. את המועדפים שלכם מקבלים עם client.gallery.favorites() או עם client.jobs.list({ favorited: true }).
איך בודקים את ההתקדמות של הורדת סרטון?
המתודה client.media.downloadVideo מחזירה downloadId. עברו בלולאה על client.media.downloadVideoProgress(downloadId) כדי לקבל את השלב ואת האחוז, עד שההורדה מסתיימת או נכשלת.
דפים קשורים
קולות ואודיו
משימות והרצות
העלאות
קול ומדיה
הוספת כתוביות
עודכן לאחרונה