# Recast ב-SDK

> אומדן מחיר, קנייה ובדיקת הסטטוס של הרצות Recast מ-TypeScript, מענה לנקודות הבחירה, ייבוא תסריט כתוב ושינוי מיקס המוזיקה בהרצה שהסתיימה.

Source: https://nodaro.ai/he/docs/developers/sdk/recast

**`client.recast`** מריץ Recast: הוא יוצר מחדש סרטון מקור שנותח, עם הליהוק שלכם, והוא יכול גם לרנדר תסריט שכתבתם, ״סרט כ-JSON״. מקבלים אומדן מחיר להרצה, קונים אותה, בודקים את ההתקדמות שלה ועונים על נקודות הבחירה שלה: הליהוק, גיליונות הזהות, תמונות הסטילס של הסצנות והמוזיקה. המתודות קוראות לנקודות הקצה שבדף [ה-API של Recast](https://nodaro.ai/docs/developers/api/recast). המדריך לכתיבת תסריטים נמצא בדף [Recast דרך MCP](https://nodaro.ai/docs/mcp/recast).

Recast רץ ב-Nodaro Cloud. בהתקנה באירוח עצמי, המתודות האלה זורקות `NotFoundError`.

## מתודות
| מתודה | מה היא עושה |
| --- | --- |
| [`authoringSkill()`](#authoringskill) | קריאת המדריך לכתיבת תסריט |
| [`validateScript(script)`](#validatescriptscript) | בדיקת תסריט כתוב |
| [`importScript(script, opts)`](#importscriptscript-opts) | שמירת תסריט כתוב כניתוח |
| [`estimate(input)`](#estimateinput) | אומדן מחיר להרצה, בחינם |
| [`create(input)`](#createinput) | קניית התוכנית ויצירת ההרצה |
| [`get(recastId)`](#getrecastid) | קריאת הסטטוס של ההרצה ושל נקודת הבחירה שממתינה |
| [`start(recastId, opts?)`](#startrecastid-opts) | התחלת הרינדור של הרצה מתוכננת |
| [`resolveGate(recastId, input)`](#resolvegaterecastid-input) | מענה לנקודת בחירה שממתינה |
| [`estimateRescore(recastId, input)`](#estimaterescorerecastid-input) | אומדן מחיר למיקס חדש או לרצועת מוזיקה חדשה |
| [`rescore(recastId, input)`](#rescorerecastid-input) | החלת שינוי האודיו שקיבלתם עליו אומדן מחיר |

## תסריטים כתובים
### authoringSkill()‎
מחזירה את המדריך לכתיבה, כ-Markdown (`GET /v1/video-analysis/authoring-skill`). המדריך מכסה את מסמך התסריט, את הערכים המותרים ואת המגבלות שלו, את כללי האודיו ודוגמה שנבדקה. המתודה חינמית.

```ts
authoringSkill(): Promise<string>
```

```ts
const guide = await client.recast.authoringSkill()
```

תנו את המדריך למודל השפה שכותב את התסריט שלכם.

### validateScript(script)‎
בודקת תסריט כתוב ומחזירה `{ valid, errors, warnings }` (`POST /v1/video-analysis/import/validate`). המתודה חינמית ולא שומרת כלום.

```ts
validateScript(script: Record<string, unknown>): Promise<{
valid: boolean
errors: Array<{ path: string; message: string; hint?: string }>
warnings: string[]
}>
```

<TypeTable
type={{
script: { type: 'Record<string, unknown>', required: true, description: "מסמך התסריט." },
}}
/>

```ts
let check = await client.recast.validateScript(script)
while (!check.valid) {
script = await fixWithModel(script, check.errors) // each error has a path, a message and usually a hint
check = await client.recast.validateScript(script)
}
```

כל שגיאה מציינת ב-`path` את המקום השגוי, ובדרך כלל כוללת גם `hint` שנכתב בשביל מודל שפה. תקנו ובדקו שוב, עד שהשדה `valid` הוא `true`.

### importScript(script, opts)‎
שומרת תסריט תקין כמשימת ניתוח שהושלמה (`POST /v1/video-analysis/import`). העבירו את ה-`jobId` שהמתודה מחזירה בתור ה-`analysisJobId` של הרצת Recast. המתודה חינמית.

```ts
importScript(script: Record<string, unknown>, opts: { rightsAttested: true }): Promise<{
jobId: string
created: boolean
warnings: string[]
json: Record<string, unknown>
}>
```

<TypeTable
type={{
script: { type: 'Record<string, unknown>', required: true, description: "מסמך תסריט תקין." },
rightsAttested: { type: 'true', required: true, description: "אתם מאשרים שהיצירה שייכת לכם. בלי השדה הזה, הייבוא נכשל עם ForbiddenError." },
}}
/>

```ts
const { jobId: analysisJobId, json } = await client.recast.importScript(script, { rightsAttested: true })
```

סרט Recast מתסריט כתוב מרונדר בדיוק כפי שנכתב, ולכן ייבאו רק יצירות שבבעלותכם. השדה `json` הוא המסמך שלכם, עם השדות שהשרת מוסיף; העדיפו אותו על פני הקלט שלכם. ייבוא חוזר של אותו תסריט מחזיר `created: false`.

## ההרצה
### estimate(input)‎
נותנת אומדן מחיר להרצה, בקרדיטים (`POST /v1/recast/estimate`). המתודה חינמית. גוף הבקשה זהה לזה של `create()`, בלי `workflowId`, `rightsAttested` ו-`clientCapabilities`.

```ts
estimate(input: EstimateRecastInput): Promise<{ totalCredits?: number; breakdown?: Record<string, number> }>
```

<TypeTable
type={{
analysisJobId: { type: 'string', required: true, description: "ניתוח הווידאו או התסריט שיובא, שעליו מריצים Recast." },
fidelity: { type: 'string', description: "כמה ההרצה נצמדת למקור. בתסריט כתוב משתמשים ב-faithful." },
resolution: { type: 'string', description: "רזולוציית הפלט." },
segmentSec: { type: 'number', description: "איך הרינדור מקבץ את הסצנות לחלקים. כאן השרת מקבל שם של שיטת קיבוץ: scenes-max (הכי מעט נקודות חיבור), scenes (חלקים קצרים יותר) או max (החלקים הארוכים ביותר שהמודל מאפשר). ראו את ההערה למטה." },
renderMethod: { type: 'string', description: "שיטת הרינדור." },
interactive: { type: 'boolean', description: "עצירה בנקודות הבחירה, כדי שתבחרו בעצמכם." },
provider: { type: 'string', description: "מודל הווידאו." },
}}
/>

```ts
const quote = await client.recast.estimate({ analysisJobId, fidelity: "faithful" })
console.log(quote.totalCredits, quote.breakdown)
```

**`segmentSec` ב-SDK 2.17.0.** נתיבי Recast מקבלים ב-`segmentSec` שם של שיטת קיבוץ: `"scenes-max"`, `"scenes"` או `"max"`. ב-SDK 2.17.0 השדה מוגדר בטיפוסים כמספר, ולכן העבירו את השם עם המרת טיפוס, למשל `segmentSec: "scenes" as unknown as number`, או השמיטו את השדה כדי להשתמש בברירת המחדל של השרת. מספר כמו `10` נדחה עם 400. אותו דבר חל על `create()` ועל `start()`.

### create(input)‎
יוצרת את ההרצה ו**קונה את התוכנית** (`POST /v1/recast`). קבלו קודם אומדן מחיר עם `estimate()`.

```ts
create(input: CreateRecastInput): Promise<{ recastId: string }>
```

<TypeTable
type={{
workflowId: { type: 'string', required: true, description: "תהליך קיים שבבעלותכם. ההרצה משויכת אליו." },
analysisJobId: { type: 'string', required: true, description: "ניתוח הווידאו או התסריט שיובא, שעליו מריצים Recast." },
rightsAttested: { type: 'boolean', description: "אתם מאשרים שהזכויות על המקור בידיכם." },
clientCapabilities: { type: 'string[]', description: "סוגי נקודות הבחירה שהלקוח שלכם יכול לענות עליהם, למשל sheet-gate. בנקודות בחירה שלא הצהרתם עליהן, ההכרעה אוטומטית." },
'...': { type: 'EstimateRecastInput', description: "השדות של estimate()‎." },
}}
/>

```ts
const { recastId } = await client.recast.create({
workflowId,
analysisJobId,
fidelity: "faithful",
rightsAttested: true,
interactive: true,
clientCapabilities: ["sheet-gate"],
})
```

בלי `workflowId`, הקריאה נכשלת עם `400 workflow_id_required`; עם תהליך לא מוכר או של מישהו אחר, היא נכשלת עם 404.

### get(recastId)‎
מחזירה את ההרצה (`GET /v1/recast/:id`). קראו למתודה הזו מדי כמה שניות כדי לבדוק את ההתקדמות. בהרצה אינטראקטיבית, השדה `interactive.next` מציין את השלב או את נקודת הבחירה שממתינים.

```ts
get(recastId: string): Promise<RecastRunSnapshot>
```

<TypeTable
type={{
recastId: { type: 'string', required: true, description: "המזהה של הרצת ה-Recast." },
}}
/>

```ts
const run = await client.recast.get(recastId)
console.log(run.status, run.interactive)
```

בתמונת המצב יש `status`, `interactive`, `capabilities`, ובהרצה שהסתיימה עם שכבות אודיו נפרדות, גם `audio`. ראו את הסעיף [שינוי מיקס המוזיקה](#change-the-music-mix).

### start(recastId, opts?)‎
מתחילה את הרינדור של הרצה במצב `planned` (`POST /v1/recast/:id/start`). המחיר של הרינדור כבר נכלל באומדן של התוכנית, וקריאה חוזרת לא משנה כלום.

```ts
start(recastId: string, opts?: { segmentSec?: number; provider?: string }): Promise<{ gvpJobId?: string }>
```

<TypeTable
type={{
recastId: { type: 'string', required: true, description: "המזהה של הרצת ה-Recast." },
segmentSec: { type: 'number', description: "איך הרינדור מקבץ את הסצנות לחלקים: שם של שיטת קיבוץ, כמו ב-estimate()‎." },
provider: { type: 'string', description: "מודל הווידאו." },
}}
/>

```ts
const { gvpJobId } = await client.recast.start(recastId)
```

### resolveGate(recastId, input)‎
עונה על נקודת בחירה שממתינה בהרצה אינטראקטיבית (`POST /v1/recast/:id/select`). הפלטפורמה מקדמת בעצמה כל שלב אחר, כך שלקוח רק קורא ל-`get()` מדי כמה שניות ועונה על נקודות בחירה. הבחירה עצמה חינמית.

```ts
resolveGate(recastId: string, input: ResolveRecastGateInput): Promise<Record<string, unknown>>
```

<TypeTable
type={{
recastId: { type: 'string', required: true, description: "המזהה של הרצת ה-Recast." },
gate: { type: '"cast" | "sheet" | "anchors" | "music"', description: "נקודת הבחירה שאתם עונים עליה." },
picks: { type: 'unknown', description: "הבחירה שלכם בנקודת בחירה של ליהוק או של גיליון זהות." },
segment: { type: 'number', description: "החלק שנקודת הבחירה שייכת אליו." },
anchorPicks: { type: '{ start?: number; end?: number }', description: "תמונות הסטילס שבחרתם, בנקודת בחירה מסוג anchors." },
section: { type: 'number', description: "קטע המוזיקה שנקודת הבחירה שייכת אליו." },
musicPick: { type: 'number | string', description: "הבחירה שלכם בנקודת בחירה של מוזיקה." },
finishAuto: { type: 'boolean', description: "העברה של נקודת הבחירה הזו ושל כל הנקודות שנותרו למבקר האוטומטי." },
}}
/>

```ts
await client.recast.resolveGate(recastId, { gate: "music", musicPick: 1 })
await client.recast.resolveGate(recastId, { finishAuto: true })
```

נקודת בחירה נפתחת רק בסוגים שהקריאה שלכם ל-`create()` הצהירה עליהם ב-`clientCapabilities`. בכל השאר, הפלטפורמה מכריעה אוטומטית.

## שינוי מיקס המוזיקה
בהרצת Recast שהסתיימה, המוזיקה והאודיו של הסרטון עצמו יכולים להישמר כשכבות נפרדות. כשהמתודה `get()` מחזירה `capabilities.audioLayers` עם הערך `1`, המניפסט `audio` מתאר אותן:

- **`revision`** מזהה את האודיו הנוכחי. כל שינוי דורש אותו.
- **`present`** מפרט את השכבות שיש בהרצה, `music` ו-`video`.
- **`layers`** כולל קובצי תצוגה מקדימה של השכבות הקיימות.
- **`bakedEffectiveGain`** מתאר את רמות העוצמה בקובץ הנוכחי להורדה.
- **`pendingRescore`** מתאר שינוי שעדיין מתבצע. הוא נשאר גם אחרי טעינה מחדש של הדף.

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

### estimateRescore(recastId, input)‎
נותנת אומדן מחיר לשינוי אודיו: מיקס חדש, רצועת מוזיקה חדשה או שניהם (`POST /v1/recast/:id/estimate-rescore`). המתודה חינמית ומחזירה `{ credits, audioRevision, noOp }`.

```ts
estimateRescore(recastId: string, input: EstimateRecastRescoreInput): Promise<{ credits: number; audioRevision: string; noOp: boolean }>
```

<TypeTable
type={{
recastId: { type: 'string', required: true, description: "המזהה של הרצת ה-Recast." },
expectedAudioRevision: { type: 'string', required: true, description: "הערך של audio.revision שקראתם." },
mix: { type: '{ music?: { gain, muted }, video?: { gain, muted } }', description: "המיקס המלא שאתם רוצים, עם ערך gain ומתג השתקה לכל שכבה." },
audioUrl: { type: 'string', description: "רצועת מוזיקה חדשה שמחליפה את המוזיקה." },
sections: { type: 'Array<{ index: number; brief: string }>', description: "קטעי מוזיקה ליצירה מחדש, כל אחד עם בריף קצר. השתמשו או ב-audioUrl או ב-sections." },
}}
/>

### rescore(recastId, input)‎
מחילה את שינוי האודיו (`POST /v1/recast/:id/rescore`). שלחו את אותה פעולה שקיבלתם עליה אומדן מחיר, ועוד `requestId` חדש, מסוג UUID. השתמשו שוב באותו `requestId` רק כדי לנסות שוב את אותה בקשה אחרי כשל ברשת.

```ts
rescore(recastId: string, input: EstimateRecastRescoreInput & { requestId: string }): Promise<
| { recastId: string; jobId: string }
| { recastId: string; noOp: true; audioRevision: string }
>
```

<TypeTable
type={{
recastId: { type: 'string', required: true, description: "המזהה של הרצת ה-Recast." },
requestId: { type: 'string', required: true, description: "מזהה UUID חדש לשינוי הזה." },
'...': { type: 'EstimateRecastRescoreInput', description: "אותה פעולה ששלחתם ל-estimateRescore." },
}}
/>

```ts
const { total: available } = await client.credits.balance()
const run = await client.recast.get(recastId)

if (run.capabilities?.audioLayers === 1 && run.audio) {
const operation = {
expectedAudioRevision: run.audio.revision,
mix: {
music: { gain: 55, muted: false },
video: { gain: 100, muted: false },
},
}
const quote = await client.recast.estimateRescore(recastId, operation)
if (!quote.noOp && available >= quote.credits) {
await client.recast.rescore(recastId, { ...operation, requestId: crypto.randomUUID() })
}
}
```

השינוי רץ כמשימה. קראו ל-`get(recastId)` מדי כמה שניות, עד שהשדה `audio.pendingRescore` נעלם והשדה `audio.revision` משתנה. שינוי שלא היה משנה כלום מחזיר `noOp: true`, בלי משימה.

- פעולה כוללת מיקס, החלפת מוזיקה אחת (`audioUrl` או `sections`) או החלפה יחד עם המיקס שלה.
- עם החלפה, שלחו את המיקס המלא שאתם רוצים. השמטה שלו עובדת רק כשהרמות הן בדיוק רמות ברירת המחדל הישנות, ובכל מקרה אחר הבקשה נכשלת עם `409 legacy_mix_mismatch`.
- בקשה עם `expectedAudioRevision` לא עדכני נכשלת עם `409 stale_audio_revision`, ושינוי שני בזמן ששינוי אחר רץ נכשל עם `409 rescore_in_progress`. טענו שוב את ההרצה, ואז קבלו אומדן מחיר חדש.
- פעולה לא תקינה נכשלת עם 400, למשל `validation_error`, `unknown_section`, `duplicate_section` או `all_audio_silent`.
- באומדן מחיר אף פעם לא משוריינים קרדיטים, והוא מחזיר את המחיר גם כשהיתרה שלכם נמוכה מדי.

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

### מה עושה Recast?

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

### האם יצירה של הרצת Recast עולה קרדיטים?

כן. המתודה create קונה את התוכנית, ולכן קבלו קודם אומדן מחיר עם המתודה client.recast.estimate, שלא עולה כלום. גם מענה לנקודת בחירה עם המתודה resolveGate לא עולה כלום.

### האם אפשר להריץ Recast על תסריט משלכם, במקום על סרטון אמיתי?

כן. בדקו את התסריט עם המתודה validateScript עד שהוא תקין, ואז ייבאו אותו עם המתודה importScript, כשהערך של rightsAttested הוא true. הייבוא מחזיר jobId, ואותו מעבירים בתור analysisJobId של הרצת Recast חדשה.

### איך משנים את עוצמת המוזיקה בהרצת Recast שהסתיימה?

טענו את הרצת ה-Recast עם המתודה get, קבלו אומדן מחיר למיקס החדש עם estimateRescore, ואז שלחו את אותה פעולה ל-rescore עם requestId חדש. קראו ל-get מדי כמה שניות, עד שגרסת האודיו משתנה.
