# 3D 장면

> client.scene3d와 3D 장면 노드로 TypeScript에서 프롬프트로 편집 가능한 3D 장면을 생성하고, 편집하고, MP4로 렌더링하며, 3D 렌더링 Pro를 실행합니다.

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

<strong>`client.scene3d`</strong>는 편집 가능한 3D 장면을 만듭니다. 프롬프트로 장면을 제작하고, 지시나 정확한 연산으로 편집하며, 리비전을 MP4로 렌더링합니다. 장면을 제작하고 렌더링까지 하는 하나의 작업인 **3D 렌더링 Pro**도 실행하고, 렌더링이 전달한 파일도 읽습니다. 이 메서드들은 [**3D 장면 생성**](https://nodaro.ai/docs/nodes/video/generate-3d-scene), [**3D 장면 편집**](https://nodaro.ai/docs/nodes/video/edit-3d-scene), [**동영상 렌더링**](https://nodaro.ai/docs/nodes/video/render-video), [**3D 렌더링 Pro**](https://nodaro.ai/docs/nodes/video/pro-3d-render) 노드를 사용합니다. 엔드포인트는 [3D 장면 REST 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) | 3D 렌더링 Pro 실행의 가격을 매깁니다 |
| [`runPro(params, options?)`](#runproparams-options) | 견적을 받은 3D 렌더링 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`로도 할 수 있습니다. 내 페이지에서 사람들이 장면을 회전하고 조정할 수 있게 하려면 [3D 미리 보기 임베드](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")
```

- <strong>`basic`</strong>은 결정론적 엔진입니다. `{ available, sceneSchemaVersions }` 형태입니다.
- <strong>`advanced`</strong>는 선택적인 엔진을 나열하며, 엔진이 없으면 `null`입니다. 사용할 수 없는 엔진을 명시적으로 지정하면 기본 엔진의 생성이나 그 크레딧 확인보다 먼저 거부됩니다.
- <strong>`pro`</strong>는 3D 렌더링 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: "사용할 기존 GLB 모델이며, 최대 8개입니다. 각각 { 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 }],
})
```

<strong>`inputAssets`</strong>는 내가 사용할 수 있는 GLB 모델을 변하지 않는 ID로 선택합니다. 외관 이미지와 모션 동영상은 여전히 `references`로 전달합니다. 에셋 URL이나 해시는 보내지 마세요. 바이트 기록은 서버가 제공합니다. 기본 엔진과 가져오기를 지원하지 않는 엔진은 요금을 청구하기 전에 `inputAssets`를 거부합니다. Pro 견적을 요청할 때도 같은 선택자를 다시 사용하세요.

### 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: "지시입니다. 예: “카메라를 더 느리게”." },
operations: { type: 'Scene3DEditOperation[]', description: "정확한 편집입니다. 예: { op: set-camera, changes: { focalLengthMm: 50 } }." },
references: { type: 'Scene3DReference[]', description: "추가할 레퍼런스이며, ID 기준으로 병합됩니다." },
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: "플랜이 3D 장면임을 표시합니다." },
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: "새 리비전의 ID입니다. 같은 편집을 다시 시도할 때는 같은 값을 유지하세요." },
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,
})
```

사용자가 아직 편집을 시작한 리비전을 편집 중일 때만 반환된 장면을 채택하세요. 형상과 카메라 파일은 재사용됩니다. 포스터, 검증 결과, 네이티브 다운로드는 새 리비전용으로 만들어진 뒤에야 다시 첨부됩니다.

## 3D 렌더링 Pro
3D 렌더링 Pro는 하나의 작업입니다. `source`를 넣으면 작업 하나가 정확한 구성인 `scenePlan`과 MP4인 `videoUrl`을 모두 담아 완료됩니다. 결과에는 리비전, 포스터, `shotStills`, 검증 결과, 렌더러 정보도 담깁니다. `client.nodes.run("pro-3d-render")`와 `runAndWait`도 같은 타입의 매개변수로 같은 라우트를 호출합니다.

엔진이 없는 설치는 `503 SCENE_CAPABILITY_UNAVAILABLE`로 응답하며, 기본 엔진으로 대체하지 않습니다. 가격이 설정되지 않은 설치는 무엇이든 예약하기 전에 `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`는 다음 세 가지 형태 중 하나입니다.

- <strong>`{ kind: "prompt", prompt, references? }`</strong>는 새 장면을 제작합니다.
- <strong>`{ kind: "scene", revisionId, sourceJobId }`</strong>는 `editPrompt`가 없으면 제작 요금 없이 보관된 장면을 내보냅니다. `editPrompt`를 추가하면 먼저 장면을 수정합니다. 작업 기록에만 있는 기본 엔진 장면에는 `sourceJobId`가 필요합니다.
- <strong>`{ kind: "local-export", exportId, connectionId }`</strong>는 페어링된 데스크톱을 사용합니다.

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

### runPro(params, options?)
견적을 받은 실행을 시작합니다. `quotePro()`의 `quoteId`가 필요하므로, 아무도 보지 않은 가격으로 실행이 시작되는 일은 없습니다. `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`가 없으면 견적을 받고, 실행하고, 기다립니다. 전체 작업을 한 번의 호출로 처리합니다. 요청은 최대 2번 보내며, 가격을 매긴 내용과 정확히 같은 것의 해시를 기준으로 승인된 유료 작업 하나를 시작합니다.

```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
}
```

그래도 스틸을 모델의 레퍼런스로 사용할 수 있습니다. 읽은 URL을 그대로 `referenceImageUrls`에 넣거나 노드의 `stills` 출력으로 전달하세요. 플랫폼은 내 이름으로 그 실행에 그 파일 하나를 잠시 읽을 권한을 줍니다. 이 권한은 몇 분 뒤에 만료되므로, 권한이 아니라 인증이 필요한 URL을 저장하세요.

### 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`** 경고는 프롬프트가 말하지 않아 실행이 결정한 내용을 나열합니다. 경고 코드는 추가될 수 있으므로, 모르는 코드는 오류가 아니라 참고 정보로 취급하세요.
- <strong>`repairPasses`</strong>는 제작 패스가 아니라 수정 횟수를 세므로, `0`은 장면이 처음에 바로 승인되었다는 뜻입니다. `admissionRetries`는 다른 것을 셉니다. 빌드 전에 플래너에게 레시피를 다시 요청한 횟수입니다.
- <strong>`mechanicalPasses`</strong>는 플래너 없이 컴파일러 자체의 해결책으로 엔진이 적용한 수정 횟수입니다. 이 수정에는 최대 2회의 별도 견적 허용량이 있고, 쓰지 않으면 해제되며, 한 번마다 `REMEDY_AUTO_APPLIED` 경고가 추가됩니다. 이 허용량이 생기기 전에 견적을 받은 실행은 이런 패스를 수정으로 청구했습니다. 받은 견적을 읽어 구분하세요.
- <strong>`restoredAssertions`</strong>는 플래너가 변경을 요청받지 않은 필수 검사를 바꾼 뒤 엔진이 되돌려 놓은 검사를 나열합니다. 각각 `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`는 필수 검사가 실패했거나 컴파일러가 레시피를 거부했다는 뜻입니다. 그렇게 실패한 결과에 무엇이 남는지는 [3D 렌더링 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: "렌더링의 작업 ID입니다." },
}}
/>

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

파일은 네 종류입니다. 모든 결과물에 있는 `validation-report`, 거부된 결과물을 제외한 모든 결과물에 있는 `poster`, 샷마다 하나씩 있고 각각 `shotIndex`, `frame`, `width`, `height`가 딸린 `shot-still`, 그리고 `refused-authoring` 결과물에만 있는 `source-json`입니다. `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: "렌더링의 작업 ID입니다." },
asset: { type: 'Scene3DDeliveryAsset', required: true, description: "getDelivery()에서 받은 파일 디스크립터이며, 그대로 전달합니다." },
signal: { type: 'AbortSignal', description: "다운로드를 중지합니다." },
}}
/>

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

### retainedRecipe(jobId, options?)
거부된 3D 렌더링 Pro 실행이 보관한 레시피를 파싱해 읽으며, 레시피가 없으면 `null`을 반환합니다. 거부된 실행의 레시피는 컴파일되지 않았으므로 장면 리비전도, 포스터도, 소스 파일도 없습니다. 레시피가 그 실행이 남기는 것입니다.

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

<TypeTable
type={{
jobId: { type: 'string', required: true, description: "거부된 실행의 작업 ID입니다." },
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로 프롬프트를 3D 애니메이션으로 만들려면 어떻게 하나요?

프롬프트와 함께 client.scene3d.generateAndWait를 실행하면 편집 가능한 장면 플랜을 받습니다. 원하면 editAndWait로 변경한 다음, renderAndWait로 MP4로 렌더링하세요.

### 3D 장면을 렌더링하면 비용이 얼마나 드나요?

가격은 플랜의 프레임 크기에 따라 정해집니다. 긴 변이 1,920픽셀 이하이면 55크레딧, 5.12메가픽셀 이하이면 83크레딧, 그보다 크면 138크레딧입니다. 현재 가격은 모델 비용 API에서 확인하세요.

### 3D 렌더링 Pro는 무엇인가요?

장면을 제작하고 렌더링까지 하는 하나의 작업이며, 결과에 장면 플랜과 MP4가 모두 담깁니다. 먼저 quotePro로 견적을 받으세요. 내 설치에서 제공되는지는 capabilities().pro로 확인하세요.

### 샷 스틸의 URL을 img 태그로 열 수 있나요?

아니요. 샷 스틸과 그 밖의 결과물 파일은 인증이 필요한 엔드포인트에서 제공됩니다. 작업을 실행할 때 쓴 것과 같은 자격 증명으로 가져온 다음, 그 바이트를 표시하거나 저장하세요.
