# סצנות תלת־ממד

> צרו מפרומפט סצנת תלת־ממד שאפשר לערוך, ערכו אותה ורנדרו אותה ל-MP4, והריצו רינדור תלת־ממד Pro מ-TypeScript עם client.scene3d ורכיבי התלת־ממד.

Source: https://nodaro.ai/he/docs/developers/sdk/scenes-3d

**`client.scene3d`** מפיק סצנות תלת־ממד שאפשר לערוך: הוא יוצר סצנה מפרומפט, עורך אותה לפי הוראה או בפעולות מדויקות, ומרנדר גרסה שלה ל-MP4. הוא גם מריץ **רינדור תלת־ממד Pro**, פעולה אחת שיוצרת סצנה ומרנדרת אותה, וקורא את הקבצים שרינדור מסר. המתודות משתמשות ברכיבים [יצירת סצנת תלת־ממד](https://nodaro.ai/docs/nodes/video/generate-3d-scene), [עריכת סצנת תלת־ממד](https://nodaro.ai/docs/nodes/video/edit-3d-scene), [רינדור וידאו](https://nodaro.ai/docs/nodes/video/render-video) ו[רינדור תלת־ממד Pro](https://nodaro.ai/docs/nodes/video/pro-3d-render). נקודות הקצה מפורטות בדף ה-API [סצנות תלת־ממד](https://nodaro.ai/docs/developers/api/3d-scenes).

## מתודות
| מתודה | מה היא עושה |
| --- | --- |
| [`capabilities()`](#capabilities) | קריאת המנועים והאפשרויות של Pro שההתקנה הזו מציעה |
| [`generate(params)` ו-`generateAndWait()`](#generateparams-and-generateandwaitparams-options) | יצירת סצנה חדשה מפרומפט |
| [`edit(params)` ו-`editAndWait()`](#editparams-and-editandwaitparams-options) | יצירת גרסה חדשה של סצנה |
| [`render(params)` ו-`renderAndWait()`](#renderparams-and-renderandwaitparams-options) | רינדור של גרסה ל-MP4, בלי יצירה |
| [`applyEdits(revisionId, params)`](#applyeditsrevisionid-params) | שמירת עריכות מדויקות בלי יצירה ובלי חיוב |
| [`quotePro(params)`](#quoteproparams) | אומדן מחיר להרצה של רינדור תלת־ממד Pro |
| [`runPro(params, options?)`](#runproparams-options) | התחלת הרצה של רינדור תלת־ממד Pro שקיבלה אומדן מחיר |
| [`renderProAndWait(params, options?)`](#renderproandwaitparams-options) | אומדן מחיר, הרצה והמתנה בקריאה אחת |
| [`getDelivery(jobId)`](#getdeliveryjobid) | קריאת הקבצים שרינדור מסר |
| [`deliveryAssetBytes(jobId, asset, options?)`](#deliveryassetbytesjobid-asset-options) | הורדה של קובץ אחד שנמסר |
| [`retainedRecipe(jobId, options?)`](#retainedrecipejobid-options) | קריאת המתכון ששמרה הרצת Pro שנדחתה |
| [`assetBytes(revisionId, asset, options?)`](#assetbytesrevisionid-asset-options) | הורדה של קובץ מגרסה של סצנה |
| [`sourceBytes(revisionId, options?)`](#sourcebytesrevisionid-options) | הורדה של קובץ המקור של גרסה, בפורמט המקורי של המנוע |

## מפרומפט ל-MP4
```ts
const created = await client.scene3d.generateAndWait({
prompt: "Orbit a single box on a floor over four seconds",
durationSeconds: 4,
fps: 24,
aspectRatio: "16:9",
})

const edited = await client.scene3d.editAndWait({
scenePlan: created.scenePlan,
expectedRevisionId: created.scenePlan.revisionId,
operations: [{ op: "set-camera", changes: { focalLengthMm: 50 } }],
})

const clip = await client.scene3d.renderAndWait({ planType: "3d-scene", plan: edited.scenePlan })
console.log(clip.videoUrl)
```

אותם שלושה שלבים עובדים גם עם [`client.nodes.runAndWait()`](https://nodaro.ai/docs/developers/sdk/nodes) ועם סוגי הרכיבים `generate-3d-scene`, `edit-3d-scene` ו-`render-video`, שיש להם פרמטרים עם טיפוסים. כדי לאפשר לאנשים לסובב ולכוונן סצנה בדף שלכם, השתמשו ב[הטמעת התצוגה התלת־ממדית](https://nodaro.ai/docs/developers/embed/scene3d). ההטמעה לא צריכה עותק של מנוע הרינדור, ובהודעות שלה אין אסימונים.

## client.scene3d
### capabilities()‎
מחזירה מה ההתקנה הזו יכולה ליצור ולרנדר (`GET /v1/3d-scene/capabilities`).

```ts
capabilities(): Promise<Scene3DCapabilities>
```

```ts
const caps = await client.scene3d.capabilities()
if (caps.advanced) console.log(caps.advanced.engines) // for example ["blender-cloud"]
if (caps.pro) console.log("3D Render Pro is available")
```

- **`basic`** הוא המנוע הדטרמיניסטי: `{ available, sceneSchemaVersions }`.
- **`advanced`** מפרט מנועים אופציונליים, וערכו `null` כשאין כאלה. בקשה מפורשת למנוע שלא זמין נדחית עוד לפני יצירה במנוע הבסיסי ולפני בדיקות הקרדיטים שלה.
- **`pro`** מתאר את רינדור תלת־ממד Pro: המנועים, פרופילי האיכות, הסגנונות ויחסי הגובה־רוחב שההתקנה הזו מספקת. הציעו רק אותם. כשהשדה `pro` חסר, הרכיב לא זמין.

### generate(params)‎ ו-generateAndWait(params, options?)‎
יוצרות סצנה חדשה מפרומפט. המתודה `generate()` מחזירה את המשימה מיד, והמתודה `generateAndWait()` ממתינה לסיום שלה ומחזירה את `scenePlan` ו-`changeSummary` אופציונלי.

```ts
generate(params: GenerateScene3DParams): Promise<RunNodeResult>
generateAndWait(params: GenerateScene3DParams, options?: RunAndWaitOptions): Promise<Scene3DJobOutput>
```

<TypeTable
type={{
prompt: { type: 'string', required: true, description: "מה קורה בסצנה." },
durationSeconds: { type: 'number', description: "האורך בשניות." },
fps: { type: 'number', description: "קצב הפריימים." },
aspectRatio: { type: 'string', description: "יחס הגובה־רוחב, למשל 16:9." },
references: { type: 'Scene3DReference[]', description: "ייחוסי תמונה ווידאו, כל אחד עם תפקיד: appearance, layout או motion." },
inputAssets: { type: 'Scene3DInputAsset[]', description: "עד 8 מודלי תלת־ממד קיימים בפורמט GLB שישמשו בסצנה, כל אחד בצורה ‎{ id, revisionId, assetId, label? }‎. דורש מנוע מתקדם שיודע לייבא." },
engine: { type: '"basic" | "blender-cloud" | "blender-local"', default: '"basic"', description: "מנוע היצירה. ראו את המתודה capabilities()‎." },
acceptedSceneSchemaVersions: { type: 'number[]', description: "גרסאות הפורמט של הסצנה שהלקוח שלכם יודע לרנדר." },
maxRepairPasses: { type: 'number', description: "תקציב התיקונים של מנוע מתקדם." },
llmModel: { type: 'string', description: "מודל השפה שמתכנן את הסצנה." },
reasoningEffort: { type: 'string', description: "מאמץ החשיבה של המתכנן." },
workflowId: { type: 'string', description: "תהליך שההרצה הזו תופיע תחתיו." },
}}
/>

```ts
const { scenePlan } = await client.scene3d.generateAndWait({
prompt: "A paper boat drifts across a puddle as rain starts",
references: [{ id: "look", kind: "image", role: "appearance", url: moodImageUrl }],
})
```

**`inputAssets`** בוחר מודלי תלת־ממד בפורמט GLB שמותר לכם להשתמש בהם, לפי המזהים הקבועים שלהם; תמונות המראה וסרטוני התנועה עדיין עוברים ב-`references`. אל תשלחו כתובות URL או גיבובים של הקבצים: השרת מספק בעצמו את הרשומות של הקבצים. המנוע הבסיסי ומנועים שלא יודעים לייבא דוחים את `inputAssets` לפני כל חיוב. כשאתם מבקשים אומדן מחיר של Pro, שלחו את אותם פריטים של `inputAssets`.

### edit(params)‎ ו-editAndWait(params, options?)‎
יוצרות **גרסה חדשה** של סצנה. הסצנה שאתם מעבירים אף פעם לא משתנה. תנו הוראה ב-`prompt`, או `operations` מדויקות, ואף פעם לא את שניהם.

```ts
edit(params: EditScene3DParams): Promise<RunNodeResult>
editAndWait(params: EditScene3DParams, options?: RunAndWaitOptions): Promise<Scene3DJobOutput>
```

<TypeTable
type={{
scenePlan: { type: 'Scene3DPlan', required: true, description: "הסצנה שממנה מתחילים." },
expectedRevisionId: { type: 'string', required: true, description: "הגרסה שאתם עורכים." },
prompt: { type: 'string', description: 'הוראה, למשל "make the camera slower" (האט את המצלמה).' },
operations: { type: 'Scene3DEditOperation[]', description: "עריכות מדויקות, למשל ‎{ op: set-camera, changes: { focalLengthMm: 50 } }‎." },
references: { type: 'Scene3DReference[]', description: "ייחוסים להוספה, שמתמזגים עם הקיימים לפי מזהה." },
replaceReferences: { type: 'boolean', description: "החלפה של כל סט הייחוסים במקום מיזוג. רשימה ריקה מנקה אותו." },
lockedObjectIds: { type: 'string[]', description: "אובייקטים שאסור לעריכה לשנות." },
selectedObjectIds: { type: 'string[]', description: "האובייקטים שההוראה עוסקת בהם." },
engine: { type: 'string', description: "מנוע היצירה." },
llmModel: { type: 'string', description: "המודל של המתכנן." },
}}
/>

```ts
const { scenePlan: next, changeSummary } = await client.scene3d.editAndWait({
scenePlan,
expectedRevisionId: scenePlan.revisionId,
prompt: "Make the rain heavier and lower the camera",
})
```

### render(params)‎ ו-renderAndWait(params, options?)‎
מרנדרות ל-MP4 בדיוק את הגרסה שאתם מעבירים, בלי ליצור או לבנות מחדש שום דבר (`POST /v1/render-video/plan`).

```ts
render(params: { planType: "3d-scene"; plan: Scene3DPlan; workflowId?: string; nodeId?: string }): Promise<RunNodeResult>
renderAndWait(params: RenderScene3DParams, options?: RunAndWaitOptions): Promise<NodeJobOutput>
```

<TypeTable
type={{
planType: { type: '"3d-scene"', required: true, description: "מסמן את התוכנית כסצנת תלת־ממד." },
plan: { type: 'Scene3DPlan', required: true, description: "הגרסה לרינדור." },
workflowId: { type: 'string', description: "תהליך שההרצה הזו תופיע תחתיו." },
nodeId: { type: 'string', description: "הרכיב שההרצה שייכת אליו." },
}}
/>

```ts
const { videoUrl } = await client.scene3d.renderAndWait({ planType: "3d-scene", plan: scenePlan })
```

המחיר נקבע לפי ה-`width` וה-`height` של התוכנית עצמה:

| פריים | קרדיטים | מזהה המחיר |
| --- | --- | --- |
| עד 1,920 פיקסלים בצד הארוך | 55 | `render-video` |
| גדול יותר, עד 5.12 מגה־פיקסלים | 83 | `render-video:3d-large` |
| מעל 5.12 מגה־פיקסלים | 138 | `render-video:3d-xlarge` |

את המחירים הנוכחיים של המזהים האלה מקבלים עם [`client.credits.modelCosts()`](https://nodaro.ai/docs/developers/sdk/models-and-credits).

### applyEdits(revisionId, params)‎
שומרת עריכות מדויקות בגרסה שמורה, בלי יצירה ובלי חיוב (`POST /v1/3d-scene/revisions/:id/edits`). המתודה מחזירה את ה-`scenePlan` החדש ו-`changeSummary`.

```ts
applyEdits(revisionId: string, params: {
newRevisionId: string
expectedContentHash: string
operations: Scene3DV2EditOperation[]
lockedObjectIds?: string[]
}): Promise<{ scenePlan: Scene3DPlanV2; changeSummary: string }>
```

<TypeTable
type={{
revisionId: { type: 'string', required: true, description: "הגרסה לעריכה." },
newRevisionId: { type: 'string', required: true, description: "המזהה של הגרסה החדשה. שמרו על אותו ערך כשאתם מנסים שוב את אותה עריכה." },
expectedContentHash: { type: 'string', required: true, description: "גיבוב התוכן של הגרסה שאתם עורכים." },
operations: { type: 'Scene3DV2EditOperation[]', required: true, description: "העריכות המדויקות, לסצנות בגרסת פורמט 2." },
lockedObjectIds: { type: 'string[]', description: "אובייקטים שאסור לעריכות לשנות." },
}}
/>

```ts
const { scenePlan: saved } = await client.scene3d.applyEdits(revisionId, {
newRevisionId: crypto.randomUUID(),
expectedContentHash,
operations,
})
```

אמצו את הסצנה שחזרה רק אם המשתמש עדיין עורך את הגרסה שממנה התחלתם. בקובצי הגאומטריה והמצלמה נעשה שימוש חוזר. תמונות הפוסטר, תוצאות הבדיקה וקובצי ההורדה בפורמט המקורי מצורפים שוב רק אחרי שהם נוצרים לגרסה החדשה.

## רינדור תלת־ממד Pro
רינדור תלת־ממד Pro הוא פעולה אחת: שולחים `source`, ומשימה אחת מסתיימת גם עם `scenePlan`, הקומפוזיציה המדויקת, וגם עם `videoUrl`, ה-MP4. התוצאה כוללת גם את הגרסה, תמונת פוסטר, `shotStills`, תוצאות הבדיקה ופרטי מנוע הרינדור. המתודות `client.nodes.run("pro-3d-render")` ו-`runAndWait` מגיעות לאותו נתיב, עם אותם פרמטרים ואותם טיפוסים.

בהתקנה בלי המנוע, הבקשה נדחית עם `503 SCENE_CAPABILITY_UNAVAILABLE`, ו-Nodaro לעולם לא עובר למנוע הבסיסי כחלופה. בהתקנה שלא נקבע בה מחיר, הבקשה נדחית עם `503 price_not_configured` לפני שמשהו משוריין. אין הגדרה של מודל או של מאמץ: השרת קובע את המתכנן.

### quotePro(params)‎
נותנת אומדן מחיר להרצה **בלי להתחיל אותה**. על הקריאה הזו לא משוריינים ולא נגבים קרדיטים. התשובה כוללת `quoteId`, תקרה ב-`maxCredits`, פירוט ב-`breakdown` שאפשר להציג, וגיבוב של הקלט שמולו ההרצה תיבדק אחר כך.

```ts
quotePro(params: Pro3DRenderParams): Promise<Pro3DRenderQuote>
```

<TypeTable
type={{
source: { type: 'Pro3DRenderSource', required: true, description: "מה לרנדר. ראו את המקורות למטה." },
engine: { type: 'string', description: "מנוע מתוך capabilities().pro. מנוע לא מוכר או לא זמין נדחה, ולעולם לא מוחלף במנוע פשוט יותר." },
durationSeconds: { type: 'number', description: "האורך. במקור מסוג scene, הוא משנה את התזמון של הסצנה; השמיטו אותו כדי לשמור על התזמון שלה." },
fps: { type: 'number', description: "קצב הפריימים." },
aspectRatio: { type: 'string', description: "יחס הגובה־רוחב, מתוך capabilities().pro." },
quality: { type: 'string', description: "פרופיל איכות מתוך capabilities().pro." },
style: { type: 'string', description: "סגנון מתוך capabilities().pro." },
maxRepairPasses: { type: 'number', description: "תקציב התיקונים, מ-0 עד 2. משלמים על כל מעבר." },
acceptedSceneSchemaVersions: { type: 'number[]', description: "גרסאות הפורמט של הסצנה שהלקוח שלכם יודע לרנדר. מקור מסוג prompt מפיק סצנה בגרסת פורמט 2, ולכן כללו את 2." },
localConnectionId: { type: 'string', description: "מחשב שולחני מקושר, להרצה מקומית." },
forcePrivate: { type: 'boolean', description: "שמירה של כל קובצי ההרצה מחוץ לאחסון עם גישת קריאה ציבורית." },
}}
/>

ל-`source` יש אחת משלוש צורות:

- **`{ kind: "prompt", prompt, references? }`** יוצר סצנה חדשה.
- **`{ kind: "scene", revisionId, sourceJobId }`** בלי `editPrompt` מייצא סצנה שמורה, בלי חיוב על יצירה. הוספה של `editPrompt` משנה קודם את הסצנה. השדה `sourceJobId` נדרש בסצנות של המנוע הבסיסי שקיימות רק בהיסטוריית המשימות.
- **`{ kind: "local-export", exportId, connectionId }`** משתמש במחשב שולחני מקושר.

```ts
const quote = await client.scene3d.quotePro(params)
showPrice(quote.maxCredits, quote.breakdown)
```

### runPro(params, options?)‎
מתחילה הרצה שקיבלה אומדן מחיר. המתודה צריכה את ה-`quoteId` מ-`quotePro()`, כך ששום הרצה לא מתחילה במחיר שאף אחד לא ראה. היא שולחת `Idempotency-Key`: מפתח חדש בכל קריאה, או המפתח שלכם מ-`options.idempotencyKey`. כשאתם מנסים שוב קריאה שנתקלה בפסק זמן, השתמשו שוב במפתח שלכם.

```ts
runPro(params: Pro3DRenderParams & { quoteId: string }, options?: { idempotencyKey?: string }): Promise<RunNodeResult>
```

<TypeTable
type={{
quoteId: { type: 'string', required: true, description: "אומדן המחיר שלפיו ההרצה מאושרת." },
'...params': { type: 'Pro3DRenderParams', required: true, description: "אותו גוף בקשה שקיבלתם עליו אומדן מחיר." },
idempotencyKey: { type: 'string', description: "אסימון הניסיון החוזר שלכם להרצה הזו." },
}}
/>

```ts
await client.scene3d.runPro({ ...params, quoteId: quote.quoteId })
```

### renderProAndWait(params, options?)‎
נותנת אומדן מחיר כשהפרמטר `params` לא כולל `quoteId`, מריצה וממתינה: כל הפעולה בקריאה אחת. המתודה שולחת שתי בקשות לכל היותר ומתחילה משימה אחת בתשלום, שמאושרת מול גיבוב של בדיוק מה שקיבל אומדן מחיר.

```ts
renderProAndWait(params: Pro3DRenderParams | Pro3DRenderRunParams, options?: RunAndWaitOptions & { idempotencyKey?: string }): Promise<Pro3DRenderJobOutput>
```

<TypeTable
type={{
params: { type: 'Pro3DRenderParams', required: true, description: "ההרצה, עם quoteId או בלעדיו." },
options: { type: 'RunAndWaitOptions & { idempotencyKey? }', description: "אפשרויות התשאול ואסימון ניסיון חוזר." },
}}
/>

```ts
const caps = await client.scene3d.capabilities()
if (caps.pro?.available) {
const shot = await client.scene3d.renderProAndWait({
source: {
kind: "prompt",
prompt: "A red suitcase rolls behind a central pillar and reappears",
references: [{ id: "look", kind: "image", role: "appearance", url: appearanceImageUrl }],
},
durationSeconds: 30,
fps: 24,
aspectRatio: "21:9",
maxRepairPasses: 2,
acceptedSceneSchemaVersions: [2],
})
console.log(shot.videoUrl)        // the MP4
console.log(shot.sceneRevisionId) // export it again later, with no authoring charge
}
```

**תמונות סטילס של השוטים.** השדה `shotStills` הוא רשימה של `{ shotIndex, frame, assetId, url }`: תמונת סטילס אחת לכל שוט, בפריים שבו השוט נפתח. אותה הרצה יוצרת אותן, בלי עלות נוספת. בסצנה עם שוט אחד יש תמונת סטילס אחת, בפריים 0. בתוצאות שנוצרו לפני שהשדה נוסף אין תמונות כאלה, ולכן השתמשו ב-`shot.shotStills ?? []`.

כל `url` הוא נקודת קצה בהתקנה שלכם **שדורשת אימות**, ולא קישור ציבורי. הורידו את הקובץ עם אותם פרטי גישה ששימשו אתכם להרצה; בקשה מתג `img` או משירות של צד שלישי נדחית עם 401:

```ts
for (const still of shot.shotStills ?? []) {
const res = await fetch(still.url, { headers: { Authorization: `Bearer ${token}` } })
const bytes = await res.arrayBuffer() // use the bytes, or store them where your pipeline can read them
}
```

עדיין אפשר להשתמש בתמונת סטילס כייחוס למודל: העבירו את הכתובת שלה כמו שקיבלתם אותה, ב-`referenceImageUrls` או דרך הפלט `stills` של הרכיב. הפלטפורמה נותנת להרצה הזו הרשאה קצרת מועד לקרוא את הקובץ האחד הזה, בשמכם. ההרשאה פגה כעבור כמה דקות, ולכן שמרו את הכתובת שדורשת אימות, ולעולם לא את ההרשאה.

### מה הרצת Pro מדווחת על עצמה
הרצה שיצרה סצנה מדווחת מה היא הניחה ומה היא עשתה. התייחסו לכל שדה כאופציונלי: בהתקנה בלי מנוע מתקדם, או בתוצאה ישנה יותר, אין אף אחד מהם.

```ts
const summary = shot.metadata?.summary // what the planner says it built
const repairs = shot.repairPasses      // 0 means accepted the first time
const retries = shot.admissionRetries  // planner retries before a build
const mechanical = shot.mechanicalPasses
const restored = shot.restoredAssertions
const assumptions = (shot.validation?.warnings ?? [])
.filter((w) => w.code === "SCENE_AUTHORING_ASSUMPTION")
.map((w) => w.message)
```

- אזהרות **`SCENE_AUTHORING_ASSUMPTION`** מפרטות מה הפרומפט לא אמר, ולכן ההרצה החליטה בעצמה. קודי אזהרה חדשים יכולים להתווסף: התייחסו לקוד לא מוכר כאל מידע, לא כאל שגיאה.
- **`repairPasses`** סופר תיקונים, לא מעברי יצירה, ולכן `0` פירושו שהסצנה התקבלה כבר בפעם הראשונה. השדה `admissionRetries` סופר משהו אחר: כמה פעמים ביקשו מהמתכנן מתכון חדש, לפני שהתחילה בנייה כלשהי.
- **`mechanicalPasses`** סופר תיקונים שהמנוע החיל לפי התיקון שהמהדר עצמו הציע, בלי המתכנן. יש להם מכסה משלהם באומדן המחיר, של עד 2 מעברים, שמשתחררת אם לא נזקקים לה, וכל מעבר כזה מוסיף אזהרת `REMEDY_AUTO_APPLIED`. בהרצה שקיבלה אומדן מחיר לפני שהמכסה הזו נוספה, שילמו על מעברים כאלה כמו על תיקונים; כדי לדעת אם זה המצב, בדקו את אומדן המחיר שקיבלתם.
- **`restoredAssertions`** מפרט בדיקות חובה שהמנוע החזיר למקומן אחרי שהמתכנן שינה בדיקה שלא ביקשו ממנו לשנות. כל אחת מהן מוסיפה גם אזהרת `ASSERTION_RESTORED`.
- ייצוא של רינדור בלבד לא יצר שום דבר, ולכן אין בו ספירות ואין בו סיכום. במיוחד ב-`mechanicalPasses`, שדה חסר לא אומר `0`.

בתוצאות של `generateAndWait()` מופיעים אותם שדות כשמנוע מתקדם יצר את הסצנה. המנוע הבסיסי לא פונה לשום מודל, ואין בתוצאות שלו אף אחד מהשדות האלה.

### מסירה שהבדיקה החזותית לא אישרה
תוצאה **שהושלמה** יכולה להגיע בלי אישור של הבדיקה החזותית. בשני המקרים הסרטון אמיתי והקרדיטים נגבו. השדה `metadata.review.verdict` מציין באיזה מקרה מדובר:

- **`"refused"`**: תקציב התיקונים נגמר, כל בדיקות החובה עברו, והבדיקה החזותית עדיין הסתייגה. הסצנה נמסרה, וההכרעה הזו מצורפת אליה.
- **`"unavailable"`**: הבדיקה לא נתנה הכרעה שאפשר להשתמש בה. הערך של `reason` הוא `"provider"` כשהבדיקה בכלל לא הגיעה לספק שלה, ו-`"unusable"` כשאי אפשר היה להשתמש בתשובה. השדה `attempts` מציין כמה פעמים פנו אליה. **אף אחד לא שפט את הסצנה הזו.**

```ts

const review = scene3DReviewVerdictOf(shot)
if (review) {
console.log(scene3DReviewNote(review)) // one user-safe sentence for either verdict
if (review.verdict === "unavailable") console.log(`unreviewed (${review.reason}) after ${review.attempts} attempts`)
for (const objection of review.objections) console.log(objection.category, objection.what, objection.correction)
}
```

השתמשו בשתי פונקציות העזר מ-`@nodaro/shared` במקום לקרוא את השדות בעצמכם, כי יש שלוש דרכים לקרוא אותם שנראות נכונות, אבל הן שגויות:

- **הערך של `validation.status` הוא עדיין `"passed"`.** בדיקות החובה אכן עברו, ולכן הסצנה נמסרה.
- **הרשימה `objections` יכולה להיות ריקה.** גם הסתייגות שלא ציינה שום דבר מסוים היא הסתייגות, ולכן ספירה של אזהרות `SCENE_REVIEW_REFUSED` מחמיצה אותה.
- **ההסתייגויות בהכרעת `"unavailable"` הן לא ההכרעה.** הן מגיעות מאצוות בדיקה שענו לפני שאחת מהן נכשלה. רשימה ריקה שם פירושה שתיקה, לא אישור.

כל הסתייגות היא `{ category, what, correction?, frames }`. בהכרעת `"unavailable"`, אזהרת `SCENE_REVIEW_UNAVAILABLE` מופיעה ראשונה ב-`validation.warnings`. הסתייגות של הבדיקה החזותית כבר לא מכשילה לבדה את המשימה. הקוד `SCENE_QUALITY_FAILED` פירושו שבדיקת חובה נכשלה או שהמהדר דחה את המתכון. בדף של רכיב [רינדור תלת־ממד Pro](https://nodaro.ai/docs/nodes/video/pro-3d-render) מפורט מה תוצאה כזו שנכשלה שומרת.

## מסירות וקבצים
המתודות האלה מחזירות קבצים שהרצה כבר מסרה. הן אף פעם לא מתחילות רינדור. כל קריאה בודקת מחדש את הגישה שלכם גם למסירה וגם למקור שלה, אפילו אחרי שגרסת המקור נמחקה.

### getDelivery(jobId)‎
מחזירה את הרשומה של מסירה (`GET /v1/3d-scene/deliveries/:jobId`): ה-`sourceKind` שלה, גרסת המקור המדויקת שלה והקבצים שקובעו בה.

```ts
getDelivery(jobId: string): Promise<Scene3DDelivery>
```

<TypeTable
type={{
jobId: { type: 'string', required: true, description: "מזהה המשימה של הרינדור." },
}}
/>

```ts
const delivery = await client.scene3d.getDelivery(jobId)
const stills = delivery.assets.filter((a) => a.kind === "shot-still")
```

מופיעים ארבעה סוגי קבצים: הסוג `validation-report` בכל מסירה, `poster` בכל מסירה חוץ ממסירה של יצירה שנדחתה, `shot-still` פעם אחת לכל שוט, כל אחד עם `shotIndex`, `frame`, `width` ו-`height` משלו, ו-`source-json` רק במסירה מסוג `refused-authoring`. הערך של `sourceKind` הוא `retained-revision`, `job-output` או `refused-authoring`. במסירה של יצירה שנדחתה, השדות `sceneRevisionId` ו-`sourcePlanSha256` הם `null` ואין תמונת פוסטר, כי שום דבר לא עבר הידור.

### deliveryAssetBytes(jobId, asset, options?)‎
מורידה קובץ אחד שמופיע ברשימה של המסירה, עם אימות מחדש ומגבלת גודל.

```ts
deliveryAssetBytes(jobId: string, asset: Scene3DDeliveryAsset, options?: { signal?: AbortSignal }): Promise<ArrayBuffer>
```

<TypeTable
type={{
jobId: { type: 'string', required: true, description: "מזהה המשימה של הרינדור." },
asset: { type: 'Scene3DDeliveryAsset', required: true, description: "מתאר קובץ מ-getDelivery()‎, כפי שהוא." },
signal: { type: 'AbortSignal', description: "עוצר את ההורדה." },
}}
/>

```ts
const bytes = await client.scene3d.deliveryAssetBytes(jobId, stills[0])
```

### retainedRecipe(jobId, options?)‎
מחזירה את המתכון ששמרה הרצה של רינדור תלת־ממד Pro שנדחתה, מפוענח, או `null` כשאין כזה. המתכון של הרצה שנדחתה אף פעם לא עבר הידור, ולכן אין גרסת סצנה, אין תמונת פוסטר ואין קובץ מקור; המתכון הוא מה שההרצה משאירה אחריה.

```ts
retainedRecipe(jobId: string, options?: { signal?: AbortSignal }): Promise<unknown | null>
```

<TypeTable
type={{
jobId: { type: 'string', required: true, description: "מזהה המשימה של ההרצה שנדחתה." },
signal: { type: 'AbortSignal', description: "עוצר את ההורדה." },
}}
/>

```ts
const recipe = await client.scene3d.retainedRecipe(jobId)
```

- **המתודה דורשת הרשאת עריכה** בתהליך של המשימה. מי שיש לו הרשאה נמוכה יותר לא רואה את המתכון בכלל, ולכן התשובה היא `null`, ולא שגיאה.
- **המשימה שנכשלה מראה אם יש טעם לבקש.** הערך של `validation.sourceRetained` שלה הוא `true` כשמתכון נשמר.
- **המתכון הוא תיעוד, לא קלט.** הרצה שנדחתה לא פרסמה שום גרסה, ולכן אי אפשר להריץ אותה שוב מהמתכון. בדקו אותו כדי לראות מה נוסה וכדי לשפר את הפרומפט הבא. הקריאה לא עולה קרדיטים.

### assetBytes(revisionId, asset, options?)‎
מורידה קובץ של גרסת סצנה שמורה: מודל תלת־ממד בפורמט GLB, מסלול מצלמה, תמונת הפוסטר או דוח הבדיקה. העבירו את המתאר המדויק מאותה גרסה. ה-SDK מגביל את ההורדה לגודל המוצהר, ומנוע הרינדור של הסצנה גם בודק את גיבוב ה-SHA-256.

```ts
assetBytes(revisionId: string, asset: Scene3DAssetRef, options?: { signal?: AbortSignal }): Promise<ArrayBuffer>
```

<TypeTable
type={{
revisionId: { type: 'string', required: true, description: "גרסת הסצנה." },
asset: { type: 'Scene3DAssetRef', required: true, description: "מתאר מסוג glb, camera-track-json, poster או validation-report." },
signal: { type: 'AbortSignal', description: "עוצר את ההורדה." },
}}
/>

```ts
const glb = await client.scene3d.assetBytes(scenePlan.revisionId, glbAsset)
```

### sourceBytes(revisionId, options?)‎
מורידה את קובץ המקור של גרסה בפורמט המקורי של המנוע, למשל קובץ `.blend`, עם בדיקת הרשאה נפרדת משלו. המתודה דורשת אותה הרשאת עריכה כמו מתכון שנשמר. קובץ מקור זמין רק כשהוא מייצג בדיוק את אותה גרסה שהתקבלה.

```ts
sourceBytes(revisionId: string, options?: { signal?: AbortSignal }): Promise<ArrayBuffer>
```

<TypeTable
type={{
revisionId: { type: 'string', required: true, description: "גרסת הסצנה." },
signal: { type: 'AbortSignal', description: "עוצר את ההורדה." },
}}
/>

```ts
const blend = await client.scene3d.sourceBytes(revisionId)
```

שתי המתודות האלה, שמורידות בייטים, משתמשות בפרטי גישה עדכניים, מכבדות ביטול, וזורקות כרגיל [שגיאות עם טיפוסים](https://nodaro.ai/docs/developers/sdk/errors).

## שאלות נפוצות

### איך הופכים פרומפט לאנימציית תלת־ממד עם ה-SDK?

הריצו את המתודה client.scene3d.generateAndWait עם פרומפט כדי לקבל תוכנית סצנה שאפשר לערוך, שנו אותה עם editAndWait אם תרצו, ואז רנדרו אותה ל-MP4 עם renderAndWait.

### כמה עולה רינדור של סצנת תלת־ממד?

המחיר של רינדור סצנת תלת־ממד נקבע לפי גודל הפריים בתוכנית: 55 קרדיטים עד 1,920 פיקסלים בצד הארוך, 83 קרדיטים עד 5.12 מגה־פיקסלים ו-138 קרדיטים מעל זה. את המחירים הנוכחיים מקבלים מה-API של עלויות המודלים.

### מה זה רינדור תלת־ממד Pro?

רינדור תלת־ממד Pro הוא פעולה אחת שיוצרת סצנה ומרנדרת אותה: התוצאה כוללת גם את תוכנית הסצנה וגם את ה-MP4. קבלו קודם אומדן מחיר עם המתודה quotePro. כדי לדעת אם ההתקנה שלכם מציעה אותו, בדקו את capabilities().pro.

### האם אפשר לפתוח כתובת של תמונת סטילס של שוט בתג img?

לא. תמונות הסטילס של השוטים וקבצי מסירה אחרים מוגשים מנקודת קצה שדורשת אימות. הורידו אותם עם אותם פרטי גישה שבהם הרצתם את המשימה, ואז הציגו או שמרו את הבייטים.
