# 파이프라인

> TypeScript로 스토리 → 동영상 파이프라인을 시작하고 단계별로 추적합니다. 단계를 승인하고, 감독과 채팅하고, 완성된 타임라인을 읽습니다.

Source: https://nodaro.ai/ko/docs/developers/sdk/pipelines

<strong>`client.pipelines`</strong>는 스토리 → 동영상 파이프라인을 실행합니다. 스토리 프롬프트를 영화로 만드는, 여러 단계로 이루어진 프로덕션입니다. 파이프라인은 `script`, `characters`, `objects`, `locations`, `shot_list`, `scene_images`, `animate_audio_edit`, `post_merge`의 8개 단계를 거치며, 각 단계에서 멈춰 내 승인을 기다릴 수 있습니다. 스튜디오의 **영화 만들기**를 코드에서 사용하는 버전입니다. 이 메서드들은 [파이프라인 REST API](https://nodaro.ai/docs/developers/api/pipelines)를 호출합니다.

## 모드
| 모드 | 각 단계에서 일어나는 일 |
| --- | --- |
| `manual`(기본값) | 모든 단계가 내 승인을 기다립니다. 각 단계를 승인하세요. 스크립트는 거부할 수도 있습니다. |
| `auto` | 엔진이 모든 단계를 스스로 승인하고 끝까지 실행합니다. [매치 컷 불일치](https://nodaro.ai/docs/developers/api/pipelines#match-cut-breaks-in-the-scene-images-stage)에서만 멈추며, 모든 불일치가 수락될 때까지 기다립니다. 그동안 상태는 `running`으로 유지되고, `pendingApprovals(id)`에 `scene_images` 단계가 나타납니다. |
| `guided` | manual과 같지만, 단계에 대해 감독과 채팅하고 감독이 제안한 변경을 적용할 수도 있습니다. |

파이프라인의 상태는 `queued`, `running`, `awaiting_approval`, `completed`, `failed`, `cancelled`, `forked` 중 하나입니다.

## 메서드
| 메서드 | 스코프 | 하는 일 |
| --- | --- | --- |
| [`create(input)`](#createinput) | `pipelines:execute` | 파이프라인을 시작합니다 |
| [`get(id)`](#getid) | `pipelines:read` | 파이프라인의 상태와 크레딧을 읽습니다 |
| [`list()`](#list) | `pipelines:read` | 내 파이프라인을 나열합니다 |
| [`cancel(id)`](#cancelid) | `pipelines:execute` | 파이프라인을 중지합니다 |
| [`pendingApprovals(id)`](#pendingapprovalsid) | `pipelines:read` | 승인을 기다리는 단계를 나열합니다 |
| [`approveStage(id, stage, edits?)`](#approvestageid-stage-edits) | `pipelines:approve` | 단계를 승인하며, 선택적으로 수정을 적용합니다 |
| [`rejectStage(id, stage, feedback)`](#rejectstageid-stage-feedback) | `pipelines:approve` | 스크립트를 거부하고 다시 쓰게 합니다 |
| [`approveSubGate(id, gate)`](#approvesubgateid-gate) | `pipelines:approve` | 애니메이션 단계 안의 체크포인트를 승인합니다 |
| [`getStage(id, stage)`](#getstageid-stage) | `pipelines:read` | 단계 하나의 출력을 읽습니다 |
| [`getTimeline(id)`](#gettimelineid) | `pipelines:read` | 조립된 영화를 읽습니다 |
| [`branch(id, input)`](#branchid-input) | `pipelines:execute` | 완료된 파이프라인을 한 단계부터 다시 실행합니다 |
| [`chatStage(pipelineId, stage, message)`](#chatstagepipelineid-stage-message) | `pipelines:approve` | 감독에게 단계 변경을 요청합니다 |
| [`applyChatProposal(pipelineId, stage, turnId)`](#applychatproposalpipelineid-stage-turnid) | `pipelines:approve` | 감독이 제안한 변경을 수락합니다 |
| [`getStageChat(pipelineId, stage)`](#getstagechatpipelineid-stage) | `pipelines:read` | 단계의 채팅을 읽습니다 |

스코프는 OAuth 토큰에 적용됩니다. API 토큰과 세션은 스코프의 제한을 받지 않습니다.

## client.pipelines
### create(input)
파이프라인을 시작합니다. `auto` 모드에서는 매치 컷 불일치로 멈추지 않는 한 스스로 끝까지 실행됩니다. 상태는 `get()`을, 결과는 `getTimeline()`을 폴링하세요. `manual` 또는 `guided` 모드에서는 `pendingApprovals()`, `approveStage()`, `approveSubGate()`로 진행하세요.

```ts
create(input: PipelineInput): Promise<{ id: string }>
```

<TypeTable
type={{
story_prompt: { type: 'string', required: true, description: "스토리이며, 1~4,000자입니다." },
target_duration_seconds: { type: 'number', required: true, description: "영화의 목표 길이(초)이며, format의 범위 안이어야 합니다. 최대 600입니다." },
format: { type: '"trailer" | "short_film" | "music_video" | "reel" | "commercial"', required: true, description: "영화의 종류입니다." },
root_node_id: { type: 'string', required: true, description: "워크플로에 있는 파이프라인 루트 노드의 ID입니다." },
workflow_id: { type: 'string', description: "파이프라인이 속한 워크플로입니다." },
pipeline_type: { type: '"story_to_video" | "song_to_music_video"', default: '"story_to_video"', description: "파이프라인의 종류입니다." },
mode: { type: '"manual" | "auto" | "guided"', default: '"manual"', description: "단계를 승인하는 방식입니다." },
output_resolution: { type: 'string', default: '"720p"', description: "완성된 영화의 해상도입니다." },
language: { type: 'string', default: '"en"', description: "스크립트와 보이스의 언어입니다." },
max_cost_credits: { type: 'number', description: "전체 실행의 지출 한도이며, 단위는 크레딧입니다." },
style_directives: { type: 'object', description: "영화 전체에 대한 스타일 지시입니다." },
config: { type: 'object', description: "사용할 모델 등 파이프라인 설정을 덮어쓰는 값입니다." },
}}
/>

```ts
const { id } = await client.pipelines.create({
story_prompt: "A lighthouse keeper finds a message in a bottle that predicts tomorrow's storm.",
target_duration_seconds: 60,
format: "short_film",
root_node_id: "pipeline-root",
mode: "auto",
})
```

### get(id)
파이프라인의 현재 상태를 읽습니다. `auto` 실행을 끝까지 추적하려면 이 메서드를 폴링하세요. 매치 컷 불일치에서 멈춘 실행은 `running`으로 유지되므로 `pendingApprovals(id)`도 확인하세요.

```ts
get(id: string): Promise<PipelineRecord>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "파이프라인 ID입니다." },
}}
/>

```ts
const pipeline = await client.pipelines.get(id)
console.log(pipeline.status, pipeline.current_stage, pipeline.current_progress_message)
```

`PipelineRecord`에는 `id`, `status`, `current_stage`, `mode`, `spent_credits`, `reserved_credits`, `upfront_credit_estimate`, `failure_reason`(상태가 `failed`일 때 설정됨), `current_progress_message`가 있고, 브랜치에는 `branched_from_pipeline_id`와 `branched_from_stage`도 있습니다.

### list()
내 파이프라인을 최신 항목부터 나열합니다.

```ts
list(): Promise<PipelineRecord[]>
```

```ts
const pipelines = await client.pipelines.list()
```

### cancel(id)
실행 중인 파이프라인을 중지합니다. 사용하지 않은 예약 크레딧은 환불됩니다. 이미 끝난 파이프라인을 취소해도 아무것도 바뀌지 않습니다.

```ts
cancel(id: string): Promise<{ ok: true }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "파이프라인 ID입니다." },
}}
/>

```ts
await client.pipelines.cancel(id)
```

### pendingApprovals(id)
승인을 기다리는 단계를 각 단계의 출력과 함께 나열합니다. `auto` 실행 중에는 엔진이 단계를 스스로 승인하므로, 매치 컷 불일치로 멈추지 않는 한 목록이 비어 있습니다.

```ts
pendingApprovals(id: string): Promise<Array<{ stage_name: PipelineStageName; output: unknown }>>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "파이프라인 ID입니다." },
}}
/>

```ts
const approvals = await client.pipelines.pendingApprovals(id)
for (const { stage_name, output } of approvals) console.log(stage_name, output)
```

### approveStage(id, stage, edits?)
단계를 승인해 파이프라인이 다음으로 넘어가게 합니다. 승인하기 전에 단계의 출력을 변경하려면 JSON Patch인 `edits`를 전달하세요.

```ts
approveStage(id: string, stage: PipelineStageName, edits?: unknown): Promise<{ ok: true }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "파이프라인 ID입니다." },
stage: { type: 'PipelineStageName', required: true, description: "승인할 단계입니다. 예: script." },
edits: { type: 'JSON Patch operations', description: "단계 출력에 먼저 적용할 변경이며, JSON Patch 연산의 목록입니다." },
}}
/>

```ts
await client.pipelines.approveStage(id, "script")

await client.pipelines.approveStage(id, "script", [
{ op: "replace", path: "/title", value: "The Keeper's Warning" },
])
```

### rejectStage(id, stage, feedback)
메모와 함께 스크립트를 거부하면, 엔진이 그 메모를 반영해 스크립트를 다시 씁니다. 거부할 수 있는 단계는 `script`뿐이며, 다른 단계는 `stage_not_implemented`로 실패합니다. 스크립트는 최대 2번 거부할 수 있으며, 자동 스크립트 검토기가 이미 스크립트를 수정했다면 그보다 적게 거부할 수 있습니다.

```ts
rejectStage(id: string, stage: PipelineStageName, feedback: string): Promise<{ ok: true }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "파이프라인 ID입니다." },
stage: { type: 'PipelineStageName', required: true, description: "거부할 단계입니다. script만 가능합니다." },
feedback: { type: 'string', required: true, description: "바꿀 내용입니다." },
}}
/>

```ts
await client.pipelines.rejectStage(id, "script", "Make the story darker and more suspenseful")
```

### approveSubGate(id, gate)
`animate_audio_edit` 단계 안의 `dialogue_recheck` 같은 체크포인트를 승인해, 파이프라인이 다음 과정으로 이어지게 합니다.

`scene_images` 단계에는 자체 체크포인트인 `match_cut_break_pending`이 있으며, `approveSubGate`로는 이를 해제할 수 없습니다. 대신 각 불일치를 REST API로 수락하세요. [장면 이미지 단계의 매치 컷 불일치](https://nodaro.ai/docs/developers/api/pipelines#match-cut-breaks-in-the-scene-images-stage)를 참조하세요.

```ts
approveSubGate(id: string, gate: SubGateName): Promise<{ ok: true; gate: SubGateName; resumed_at: string }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "파이프라인 ID입니다." },
gate: { type: 'SubGateName', required: true, description: "승인할 체크포인트입니다. 예: dialogue_recheck." },
}}
/>

```ts
await client.pipelines.approveSubGate(id, "dialogue_recheck")
```

### getStage(id, stage)
단계 하나의 `status`, `output`, `critic_feedback`을 읽습니다. 스크립트나 계획을 승인하기 전에 살펴볼 때 사용하세요.

```ts
getStage(id: string, stage: PipelineStageName): Promise<{ status: string; output: unknown; critic_feedback: unknown }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "파이프라인 ID입니다." },
stage: { type: 'PipelineStageName', required: true, description: "읽을 단계입니다." },
}}
/>

```ts
const { status, output } = await client.pipelines.getStage(id, "script")
```

### getTimeline(id)
조립된 영화를 읽습니다. 순서대로 놓인 장면과 그 길이, 오디오 URL, 애니메이션 진행 상황이 담깁니다. 직접 렌더링하거나 편집기에 넘기세요.

```ts
getTimeline(id: string): Promise<PipelineTimeline>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "파이프라인 ID입니다." },
}}
/>

```ts
const timeline = await client.pipelines.getTimeline(id)
for (const scene of timeline.scenes) console.log(scene.compositeUrl, scene.durationSeconds)
```

`PipelineTimeline`에는 `fps`, `width`, `height`, `scenes`(각각 `{ compositeUrl, durationSeconds }`), `musicUrl`, `narrationUrl`, 그리고 `totalShots`, `shotsDone`, `percent`가 담긴 `animateProgress`가 있습니다.

### branch(id, input)
완료된 파이프라인을 한 단계부터 새 파이프라인으로 다시 실행합니다. 그 앞의 단계는 승인된 상태로 복사됩니다. 원래 파이프라인은 `completed`로 유지됩니다.

```ts
branch(id: string, input: { fromStage: PipelineStageName }): Promise<{ pipelineId: string; clonedStages: string[]; clonedEntities: number }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "완료된 파이프라인입니다." },
fromStage: { type: 'PipelineStageName', required: true, description: "다시 실행할 첫 단계입니다." },
}}
/>

```ts
const { pipelineId } = await client.pipelines.branch(id, { fromStage: "scene_images" })
```

### chatStage(pipelineId, stage, message)
`guided` 모드에서 감독에게 메시지를 보냅니다. 파이프라인의 모드가 `guided`이고, 단계가 `awaiting_approval` 상태여야 합니다. 답변에는 `applyChatProposal()`로 수락할 수 있는 `proposed_change`가 들어 있을 수 있습니다. 채팅은 `script`처럼 채팅을 지원하는 단계에서 사용할 수 있습니다.

```ts
chatStage(pipelineId: string, stage: ChatEnabledStage, message: string): Promise<{
turnId: string
role: "assistant"
content: string
proposed_change: ProposedChange | null
}>
```

<TypeTable
type={{
pipelineId: { type: 'string', required: true, description: "파이프라인 ID입니다." },
stage: { type: 'ChatEnabledStage', required: true, description: "논의할 단계입니다." },
message: { type: 'string', required: true, description: "내 요청입니다." },
}}
/>

```ts
const { content, proposed_change, turnId } = await client.pipelines.chatStage(
id,
"script",
"Can you make the protagonist's motivation clearer in scene 2?",
)
```

### applyChatProposal(pipelineId, stage, turnId)
감독이 이전 턴에서 제안한 변경을 수락합니다. 변경이 검사되고, 단계의 새 버전으로 저장된 다음, 단계가 승인됩니다.

```ts
applyChatProposal(pipelineId: string, stage: ChatEnabledStage, turnId: string): Promise<
| { applied: true; attemptId: string; newOutput: unknown }
| { applied: false; error: { code: string; detail?: unknown } }
>
```

<TypeTable
type={{
pipelineId: { type: 'string', required: true, description: "파이프라인 ID입니다." },
stage: { type: 'ChatEnabledStage', required: true, description: "대상 단계입니다." },
turnId: { type: 'string', required: true, description: "적용할 제안이 담긴 어시스턴트 턴입니다." },
}}
/>

```ts
const result = await client.pipelines.applyChatProposal(id, "script", turnId)
if (result.applied) console.log("Approved:", result.newOutput)
else console.log("Not applied:", result.error.code)
```

변경을 적용할 수 없지만 복구할 수 있으면 결과는 `applied: false`이며, 감독이 이미 힌트를 담은 답변을 추가해 둔 상태입니다. 복구할 수 없는 실패는 409 오류를 발생시킵니다.

### getStageChat(pipelineId, stage)
단계의 채팅 기록을 읽습니다. 첫 메시지를 보내기 전에는 비어 있습니다.

```ts
getStageChat(pipelineId: string, stage: ChatEnabledStage): Promise<{ turns: ChatTurn[] }>
```

<TypeTable
type={{
pipelineId: { type: 'string', required: true, description: "파이프라인 ID입니다." },
stage: { type: 'ChatEnabledStage', required: true, description: "대상 단계입니다." },
}}
/>

```ts
const { turns } = await client.pipelines.getStageChat(id, "script")
```

각 `ChatTurn`에는 `id`, `turn_n`, `role`, `content`, `proposed_change`, `applied_to_attempt_id`, `created_at`이 있습니다.

## 자주 묻는 질문

### Nodaro 파이프라인은 무엇인가요?

파이프라인은 스토리 프롬프트로 영화를 여러 단계에 걸쳐 만듭니다. 스크립트, 캐릭터, 오브젝트, 장소, 샷 목록, 장면 이미지, 오디오와 편집이 포함된 애니메이션, 최종 병합 단계입니다. 스튜디오의 영화 만들기를 화면 없이 실행하는 버전입니다.

### 단계마다 승인하지 않고 파이프라인을 실행하려면 어떻게 하나요?

mode를 auto로 설정해 만드세요. 엔진이 각 단계를 스스로 승인하고 끝까지 실행합니다. 상태는 client.pipelines.get(id)를 폴링해 확인하고, 결과는 getTimeline(id)로 읽으세요. 실행은 계획한 매치 컷이 맞지 않을 때만 멈춥니다. 그러면 상태는 running으로 유지되고, 모든 불일치가 수락될 때까지 pendingApprovals(id)에 scene_images 단계가 나타납니다.

### 승인하기 전에 단계의 출력을 변경하려면 어떻게 하나요?

approveStage의 세 번째 인수로 JSON Patch를 전달하세요. 단계가 승인되기 전에 그 단계의 출력에 적용됩니다.

### 파이프라인에는 어떤 OAuth 스코프가 필요한가요?

파이프라인과 단계를 읽으려면 pipelines:read, 파이프라인을 만들고 취소하고 브랜치하려면 pipelines:execute, 단계를 승인하고 스크립트를 거부하고 단계에 대해 채팅하려면 pipelines:approve가 필요합니다.
