Nodaro 문서
문서노드 레퍼런스모델템플릿AI 에이전트(MCP)개발자셀프 호스팅리서치비교
TypeScript SDK

스튜디오 프로덕션

TypeScript로 플랜에서 스튜디오 프로덕션을 만들고, 오퍼레이션으로 편집하고, 스틸과 클립을 생성하고, 계획된 프레임을 검토하고, 공유하거나 복사합니다.

Nodaro Cloud에서 사용할 수 있습니다

스튜디오 프로덕션은 설정에 영화의 샷을 담은 Nodaro 워크플로입니다. 각 샷에는 구도를 잡은 스틸, 선택 사항인 애니메이션 클립, 그리고 이를 만든 플랜, 룩, 출연진 연결, 보이스가 있습니다. client.studio는 프로덕션을 읽고 쓰므로, 스크립트, AI 어시스턴트, 스튜디오 앱이 하나의 프로덕션을 함께 다룹니다. client.shots는 공유 링크 뒤에 있는 공유 샷 레코드를 저장합니다. 이 메서드들은 스튜디오 프로덕션 REST API를 호출합니다. 작성 가이드는 MCP로 다루는 스튜디오 프로덕션을 참고하세요.

스튜디오 프로덕션은 Nodaro Cloud에서 실행됩니다. 이 경로를 제공하지 않는 곳에서는 모든 메서드가 NotFoundError를 발생시킵니다. 한 번 확인하려면 client.studio.productions.list()를 호출하세요. 프로덕션이 있는 배포 환경은 빈 페이지로 응답하고, 없는 배포 환경은 NotFoundError를 발생시킵니다.

두 가지 메서드 세트

client.studio에는 두 계층이 있습니다. 둘 다 같은 프로덕션을 다룹니다.

계층용도반환값
client.studio.productions.*프로덕션 문서: 플랜으로 만들기, 오퍼레이션으로 편집, 스틸과 클립 생성, 보이스와 음악 추가, 공유와 복사페이로드 자체
client.studio.*계획된 프레임: 기능 확인, 키프레임 생성과 검토, 번들, 편집기 저장, 리비전 확인을 거치는 링크 공유API의 { data } 래퍼

래퍼에는 타입이 지정되어 있고, 프로덕션 문서에는 없습니다. 프로덕션, 샷, 오퍼레이션은 열린 JSON인 Record<string, unknown>입니다. 분기 조건으로 쓰는 값에는 모두 타입이 지정되어 있습니다. version, rebased, receipts, warnings, 견적의 credits, 실행의 jobIds입니다. 오퍼레이션 어휘는 서버가 제공하니 skill()에서 읽으세요.

client.studio.productions의 메서드

메서드기능
skill()작성 가이드, 카탈로그, 플랜 스키마, 오퍼레이션 가이드 읽기
validatePlan(plan)플랜을 무료로 확인
list(opts?)내 프로덕션 나열
get(productionId, opts?)프로덕션 읽기
exportPlan(productionId, opts?)내보내기 단계를 계획하고 가격 확인
create(input?)프로덕션 만들기, 선택적으로 플랜에서
ops(productionId, input)오퍼레이션 일괄 처리를 적용하거나 미리 보기
reconcile(productionId)완료된 생성 반영
importPlan(productionId, plan, opts?)플랜의 장면을 프로덕션에 추가
describe(productionId, input)개요를 장면으로 만들기
generate(), generateStill(), generateClip()샷의 구도를 잡거나 애니메이션 만들기
frame(productionId, input)샷의 클립에서 스틸 가져오기
voice(productionId, input)샷의 대사 읽기
revoice(productionId, input)샷 클립의 보이스 바꾸기
music(productionId, input)영화에 음악 입히기
share(), unshare(), clone()공유 링크를 켜거나 끄기, 또는 프로덕션 복사

client.studio.productions

productions.skill()

서버가 실행 중인 버전에서 만든 작성 가이드, 전체 카탈로그, 플랜의 JSON Schema, 오퍼레이션 가이드를 반환합니다. 무료입니다.

skill(): Promise<{ skill: string; catalog: string; schema: Record<string, unknown>; operating: string; generatedFrom: object }>
const { skill, schema, operating } = await client.studio.productions.skill()

operating은 ops()가 받는 오퍼레이션을 나열합니다. 어휘를 코드에 고정하지 말고 실행할 때 읽으세요.

productions.validatePlan(plan)

플랜이 프로덕션이 되기 전에 확인합니다. 무료이고, 아무것도 저장하지 않으며, 출연진 이름을 내 라이브러리와 대조합니다. valid가 true가 될 때까지 errors를 고치며 반복한 다음, create({ plan })을 호출하세요.

validatePlan(plan: Record<string, unknown>): Promise<{
  valid: boolean
  errors: Array<{ path: string; message: string; hint?: string }>
  warnings: Array<{ path: string; message: string; hint?: string }>
  summary?: { name?: string; scenes: number; shots: number; cast: number; bound: number }
}>

속성

타입

const check = await client.studio.productions.validatePlan(plan)
if (!check.valid) console.log(check.errors)

summary.bound는 내 라이브러리의 캐릭터와 일치한 출연진 항목 수입니다.

productions.list(opts?)

내 프로덕션을 최신순으로 나열합니다.

list(opts?: { limit?: number; cursor?: string; includeArchived?: boolean }): Promise<{ data: StudioProduction[]; nextCursor?: string }>

속성

타입

const { data: productions } = await client.studio.productions.list({ limit: 20 })

productions.get(productionId, opts?)

프로덕션을 읽습니다. 순수한 읽기이며 완료된 작업을 반영하지 않으므로, 작업을 기다리고 있다면 먼저 reconcile()을 호출하세요.

get(productionId: string, opts?: { detail?: "summary" | "full"; shotId?: string }): Promise<StudioProduction>

속성

타입

const production = await client.studio.productions.get(productionId, { detail: "full" })

productions.exportPlan(productionId, opts?)

영화를 조립하는 단계를 순서대로, 가격과 함께 반환합니다. 아무것도 실행하지 않고 아무것도 쓰지 않으므로, 단계는 일반 노드 메서드로 직접 실행하세요.

exportPlan(productionId: string, opts?: { upscale?: boolean }): Promise<{
  canExport: boolean
  steps: Array<{ id: string; label: string; node: string; creditModel: string; credits: number | null; params: Record<string, unknown> }>
  resultStepId: string | null
  estimate: number | null
  unpriced: string[]
}>

속성

타입

const plan = await client.studio.productions.exportPlan(productionId)
console.log(plan.canExport, plan.estimate)
for (const step of plan.steps) console.log(step.id, step.label, step.node, step.credits)

프로덕션의 클립이 두 개 미만이면 canExport는 false입니다. 가격이 없는 단계가 하나라도 있으면 estimate는 null이고, unpriced가 그 단계들의 모델을 알려 줍니다. 일부만 더한 합계는 비용을 실제보다 적게 보여 주기 때문입니다. 각 단계의 node는 merge-video-audio, combine-videos, video-upscale 같은 노드 유형입니다.

productions.create(input?)

프로덕션을 만들고, 선택적으로 같은 호출에서 플랜을 반영합니다.

create(input?: { name?: string; plan?: Record<string, unknown> }): Promise<{
  production: StudioProduction
  warnings?: Array<{ path: string; message: string; hint?: string }>
  summary?: { shotsAdded: number; castEnrolled: number; castBound: number }
}>

속성

타입

const { production, summary } = await client.studio.productions.create({ name: "Rome chase", plan })

productions.ops(productionId, input)

프로덕션에 오퍼레이션 일괄 처리를 적용합니다. 모든 변경은 오퍼레이션이며, 위치가 아니라 샷 id, 역할 슬러그, 결과의 작업 id 같은 고정 키로 대상을 지정합니다.

ops(productionId: string, input: StudioOpsRequest): Promise<StudioOpsResponse>
ops(productionId: string, input: StudioOpsRequest & { dryRun: true }): Promise<StudioOpsDryRunResponse>

속성

타입

const result = await client.studio.productions.ops(productionId, {
  ops: [/* operations from the operating guide */],
  baseVersion: version,
  clientRequestId: crypto.randomUUID(),
})
version = result.version // carry it forward as the next baseVersion
for (const r of result.receipts) console.log(r.summary)
  • 원자적. 잘못된 오퍼레이션이 하나라도 있으면 일괄 처리 전체가 StudioOpError로 거부되며, 그 opIndex가 해당 오퍼레이션을 알려 줍니다. 아무것도 쓰이지 않습니다.
  • 리베이스. 두 사람이 한 프로덕션을 동시에 편집할 수 있습니다. 이전 버전을 기준으로 작성한 일괄 처리도 최신 버전에 적용되며, rebased는 true입니다.
  • 영수증. receipts에는 오퍼레이션마다 과거형 설명이 한 줄씩 들어 있습니다. 예를 들어 Deleted take 2 of Shot 1 (in the bin)(샷 1의 테이크 2를 삭제해 휴지통으로 옮김) 같은 줄입니다. 오퍼레이션의 영향이 지정한 대상을 넘어서면, 그 impact가 새로 고칠 keyframeIds와 shotIds를 나열합니다.
  • 응답을 그대로 채택. 내 사본을 production으로 교체하고 version을 다음 요청으로 넘기세요. 이전 사본에 병합하지 마세요.

일괄 처리 미리 보기. dryRun: true를 주면 응답은 일괄 처리가 무엇을 할지 알려 주므로, 사람이 어시스턴트의 편집을 먼저 승인할 수 있습니다. 응답에는 dryRun, version, receipts, warnings가 있고 production은 없습니다. 각 영수증에는 class가 추가됩니다. S는 안전, D는 삭제, P는 작업물에 접근할 수 있는 사람 변경, $는 크레딧 지출입니다. 또한 restorable이 추가되는데, 이 값은 오퍼레이션이 무언가를 휴지통에 넣었을 때만 있습니다. restorable ?? false로 읽으세요.

dryRun: true는 호출 자체의 객체 안에 리터럴로 쓰세요. 변수를 거쳐 전달하면 boolean으로 넓혀져, 실제로는 미리 보기를 하면서도 호출의 타입은 적용으로 지정됩니다.

미리 보기는 요청을 두 번 보냅니다. 먼저 배포 환경이 미리 보기를 지원하는지 확인하는 빈 일괄 처리를 보내고, 그다음 내 일괄 처리를 보냅니다. 그렇지 않으면 미리 보기를 지원하지 않는 배포 환경이 경고 없이 일괄 처리를 적용해 버리기 때문입니다. 오류는 두 가지가 생길 수 있습니다.

import { StudioPreviewAppliedError, StudioPreviewUnavailable } from "@nodaro/sdk"

try {
  const preview = await client.studio.productions.ops(productionId, { ops, baseVersion, dryRun: true })
  for (const r of preview.receipts) console.log(r.class, r.summary, r.restorable ?? false)
} catch (err) {
  if (err instanceof StudioPreviewUnavailable) {
    // Nothing was sent. Say that no preview is available; do not apply the batch instead.
  } else if (err instanceof StudioPreviewAppliedError) {
    // The batch was applied. Adopt err.applied.production and err.applied.version.
    // Do not send it again. When err.applied is undefined, read the production first.
  } else {
    throw err
  }
}

productions.reconcile(productionId)

마지막으로 확인한 뒤 완료된 모든 생성을 반영하고, 아직 실행 중인 것을 알려 줍니다. 앱을 열지 않고도 완료된 작업을 결과로 바꾸는 유일한 호출이며, 반영된 것이 있을 때만 씁니다.

reconcile(productionId: string): Promise<{
  landed: string[]
  pending: string[]
  failed: string[]
  warnings: string[]
  production: StudioProduction
  version: number
}>

속성

타입

const { landed, pending } = await client.studio.productions.reconcile(productionId)

landed는 미디어가 이제 프로덕션에 들어간 작업을, pending은 아직 실행 중인 작업을, failed는 실패했거나 취소된 작업을 나열합니다.

productions.importPlan(productionId, plan, opts?)

플랜의 장면을 기존 프로덕션에 추가합니다.

importPlan(productionId: string, plan: Record<string, unknown>, opts?: { mode?: "append" }): Promise<{
  production: StudioProduction
  warnings?: Array<{ path: string; message: string; hint?: string }>
  summary?: { shotsAdded: number; castEnrolled: number; castBound: number }
}>

속성

타입

await client.studio.productions.importPlan(productionId, extraScenesPlan)

productions.describe(productionId, input)

디렉터가 개요를 장면으로 만듭니다. 작업을 시작하고 곧바로 반환하며, 장면은 reconcile()로 반영됩니다. 반환되는 프로덕션에는 이 실행이 대기 중인 초안으로 기록되어 있습니다.

describe(productionId: string, input: {
  brief: string
  llmModel: string
  mode?: "append" | "replace"
  label?: string
  clientRequestId?: string
}): Promise<{ production: StudioProduction; jobId: string }>

속성

타입

const { jobId } = await client.studio.productions.describe(productionId, {
  brief: "A courier races across Rome in the rain to deliver a violin.",
  llmModel,
})

스틸과 클립 생성

generateStill()은 샷의 구도를 잡고, generateClip()은 샷에 애니메이션을 입히며, generate()는 kind에 따라 둘 중 하나를 합니다. 실행은 작업을 제출하고, 프로덕션에 대기 중 표시를 기록한 뒤 반환합니다. 몇 분씩 기다리는 일은 없습니다. 요청은 서버에서 샷 자체의 플랜, 룩, 레퍼런스로 구성되므로, 스크립트에서 호출하든 앱에서 클릭하든 같은 미디어가 나옵니다.

generate(productionId: string, input: StudioGenerateRequest): Promise<StudioGenerateResult>
generateStill(productionId: string, shotId: string, opts?: StudioGenerateOptions): Promise<StudioGenerateResult>
generateClip(productionId: string, shotId: string, opts?: StudioGenerateOptions): Promise<StudioGenerateResult>

속성

타입

import { isStudioGenerateEstimate } from "@nodaro/sdk"

const quote = await client.studio.productions.generateStill(productionId, "shot-2", { count: 2, dryRun: true })
if (isStudioGenerateEstimate(quote)) console.log(quote.credits) // null means unpriced, not free

const run = await client.studio.productions.generateStill(productionId, "shot-2", {
  count: 2,
  clientRequestId: crypto.randomUUID(),
})
  • 먼저 견적. dryRun: true는 실행 가격을 계산하고 아무것도 쓰지 않습니다. 응답은 isStudioGenerateEstimate()로 구분하세요.
  • 안전하게 재시도. 같은 clientRequestId를 쓰면 재시도는 첫 호출이 시작한 작업을 deduped: true 표시와 함께 반환하고, 아무것도 제출하거나 청구하지 않습니다. 이 값 없이 유료 호출을 재시도하지 마세요. frame()과 voice()를 포함해 이 페이지의 모든 유료 호출이 이 값을 받습니다.
  • 레인은 자동으로 정해집니다. 클립의 동영상 경로는 샷의 입력에 따라 정해지고 lane으로 반환됩니다. generate-video 또는 text-to-video입니다.

productions.frame(productionId, input)

샷의 활성 클립에서 스틸을 가져와 target이 지정한 곳에 넣습니다. 몇 초 걸리는 작업을 기다린 뒤, 변경된 프로덕션과 이미지 url을 반환합니다.

frame(productionId: string, input: {
  shotId: string
  mode?: "first" | "last" | "timestamp"
  timestamp?: number
  target?: "new-shot" | "start-frame" | "end-frame" | "still"
  clientRequestId?: string
}): Promise<StudioMediaResponse>

속성

타입

const { url } = await client.studio.productions.frame(productionId, { shotId: "shot-2", mode: "last" })

productions.voice(productionId, input)

샷의 대사를 읽고 샷에 기록합니다. 작업이 끝날 때까지 기다립니다.

voice(productionId: string, input: {
  shotId: string
  text: string
  voiceId?: string
  voiceType?: "premade" | "custom" | "library"
  ttsProvider?: string
  delivery?: Record<string, number>
  clientRequestId?: string
}): Promise<StudioMediaResponse>

속성

타입

await client.studio.productions.voice(productionId, { shotId: "shot-3", text: "We're out of time." })

productions.revoice(productionId, input)

샷의 활성 클립에 있는 보이스를 바꿉니다. 몇 분이 걸리므로 jobId를 반환하며, 새 클립은 그 대기 중 표시를 통해 반영됩니다.

revoice(productionId: string, input: { shotId: string; plan: Record<string, unknown>; clientRequestId?: string }): Promise<{ production: StudioProduction; jobId: string }>

속성

타입

const { jobId } = await client.studio.productions.revoice(productionId, {
  shotId: "shot-3",
  plan: recastPlan, // the speaker-ordered plan the voice recast route takes
})

productions.music(productionId, input)

영화에 음악을 입힙니다. 완성된 트랙은 대기 중 표시를 통해 반영됩니다.

music(productionId: string, input: {
  prompt: string
  duration?: number
  instrumental?: boolean
  vocalGender?: string
  model?: string
  clientRequestId?: string
}): Promise<{ production: StudioProduction; jobId: string }>

속성

타입

const { jobId } = await client.studio.productions.music(productionId, {
  prompt: "Tense strings building to a chase",
  instrumental: true,
})

공유와 복사

share()는 링크 공유 화면을 열고, unshare()는 다시 닫습니다. 공유는 오퍼레이션이 아니라 별도의 호출이므로, 편집의 부작용으로 작업물을 볼 수 있는 사람이 바뀌는 일은 없습니다. clone()은 내가 소유하거나 볼 수 있는 프로덕션을 내 스튜디오 프로젝트로 복사합니다.

share(productionId: string): Promise<StudioProduction>
unshare(productionId: string): Promise<StudioProduction>
clone(productionId: string, input?: { name?: string }): Promise<StudioProduction>

속성

타입

await client.studio.productions.share(productionId)
const copy = await client.studio.productions.clone(productionId, { name: "Rome chase, take 2" })

사본은 비공개이고 보관되지 않은 상태로 시작합니다. 공유와 보관 상태는 넘어가지 않습니다. 사본은 원본을 내가 볼 수 있는 범위를 기준으로 복사되므로, 다른 사람의 휴지통은 함께 오지 않습니다.

client.studio: 계획된 프레임

이 메서드들은 계획된 키프레임과 그 검토를 다룹니다. 컨트롤을 제공하기 전에 capabilities()를 확인하고, 프로덕션을 다시 열 때는 reconcile()을 한 번 호출하세요. 제출에 대한 응답이 유실되었을 수 있기 때문입니다. 여기서는 해당 메서드를 직접 호출하지 않는 한 생성을 시작하거나 후보를 채택하는 일이 없습니다.

메서드기능
capabilities()이 배포 환경이 지원하는 플랜 버전과 오퍼레이션 읽기
skill(), list(), validatePlan(), create()프로덕션 계층과 같은 읽기와 만들기를 래퍼로 반환
get(id, options?)기능 정보와 함께 프로덕션 읽기
edit(id, input)리비전 조건을 붙여 오퍼레이션 적용
saveEditorState(id, input)불러온 리비전을 기준으로 일반 편집기 필드 저장
generateKeyframe(id, input)계획된 프레임을 채택하지 않고 생성
generateShot(id, input)스틸이나 클립의 견적을 내거나 제출
acceptKeyframe(id, review, concurrency?)검토한 후보 채택
reconcile(id)아무것도 채택하지 않고 완료된 작업 기록
setShared(id, input)검토한 리비전에 묶어 공유하거나 공유 해제
clone(id, input?)저장된 프로덕션 복사
importBundle(input)이식 가능한 프로덕션 가져오기
appendBundle(id, input)프로덕션에 번들 덧붙이기

studio.capabilities()

이 배포 환경이 지원하는 플랜 버전과 계획된 프레임 오퍼레이션을 반환합니다.

capabilities(): Promise<{ data: StudioProductionCapabilities }>
const { data: caps } = await client.studio.capabilities()
if (caps.operations.generateKeyframes) showGenerateFrameButton()

operations에는 오퍼레이션마다 플래그가 하나씩 있습니다. readKeyframes, editKeyframes, generateKeyframes, acceptKeyframes, rejectKeyframes, editSequencePlans, generateLinkedClips, retakeLinkedClips, saveEditorState, revisionedSharing, editableSharedCopies, cloneLinkedProductions, importPlannedBundles, importLinkedBundles, appendPlannedBundles, appendLinkedBundles입니다. automaticAcceptance와 unattendedGeneration은 항상 false입니다.

studio.skill(), list(), validatePlan(), create()

productions.skill(), list(), validatePlan(), create()와 같은 호출이며, { data } 래퍼에 담겨 반환됩니다. list()에서는 행이 response.data.data에 있습니다.

skill(): Promise<{ data: Record<string, unknown> }>
list(options?: { limit?: number; cursor?: string; includeArchived?: boolean }): Promise<{ data: {
  data: Array<{ id: string; name: string; version: number; updatedAt: string; thumbnailUrl: string | null; shared: boolean; archived: boolean; shotCount: number }>
  nextCursor?: string
} }>
validatePlan(plan: Record<string, unknown>): Promise<{ data: { valid: boolean; errors: object[]; warnings: object[]; summary?: object } }>
create(input: { name?: string; plan?: Record<string, unknown> }): Promise<{ data: StudioProductionReply }>

속성

타입

const { data: page } = await client.studio.list({ limit: 20 })
for (const row of page.data) console.log(row.name, row.shotCount)

studio.get(id, options?)

기능 정보와 함께 프로덕션을 읽습니다. 작업을 반영하지는 않습니다.

get(id: string, options?: { detail?: "summary" | "full"; shotId?: string }): Promise<{ data: StudioProductionReply }>

속성

타입

const { data: { production } } = await client.studio.get(productionId, { detail: "full" })
const frame = production.keyframes?.[0] // { id, label, revision, previewUrl, acceptedUrl, pending, ... }

studio.edit(id, input)

리비전 조건을 붙여 오퍼레이션을 적용하고(POST .../:id/ops), 래퍼에 담아 반환합니다. remove_shot, restore_trashed, purge_trashed에는 strict와 함께 baseVersion을 쓰고, 연결된 시퀀스 세그먼트는 그 장면을 제거하기 전에 분리하세요.

edit(id: string, input: { ops: Array<{ op: string; [field: string]: unknown }>; baseVersion?: number; strict?: boolean; clientRequestId?: string }): Promise<{ data: StudioProductionReply & { version: number; rebased: boolean; receipts: object[] } }>

속성

타입

await client.studio.edit(productionId, {
  ops: [{ op: "reject_keyframe_result", keyframeId, expectedRevision, resultKey, expectedAcceptedResultKey, reason: "Face drifted" }],
  baseVersion: version,
  strict: true,
})

edit()로 보내는 계획된 프레임 오퍼레이션 몇 가지입니다.

  • reject_keyframe_result는 생성하지 않고 수정 필요를 기록합니다. operations.rejectKeyframes가 필요합니다.
  • update_sequence_plan은 시퀀스의 순서가 정해진 세그먼트(각각 { shotId, startKeyframeId, endKeyframeId })를 편집하고, 장면 id는 유지합니다. operations.editSequencePlans가 필요합니다.
  • detach_sequence_segment는 mode를 clear나 keep-accepted로 설정해 세그먼트 하나를 독립시킵니다. operations.editSequencePlans가 필요합니다.
  • purge_trashed는 내가 표시하는 휴지통 항목을 비우고, clear_trash는 계획된 프레임을 포함해 모든 휴지통을 비웁니다.

studio.saveEditorState(id, input)

불러온 리비전을 기준으로 일반 편집기 필드를 저장합니다. 먼저 operations.saveEditorState를 확인하세요. 저장은 항상 strict로 처리됩니다. 충돌하면 409로 실패하므로, 로컬 초안을 보관하고 다시 불러온 다음 충돌을 해결하세요.

saveEditorState(id: string, input: { expectedVersion: number; graph: object; clientRequestId?: string })

속성

타입

await client.studio.saveEditorState(productionId, { expectedVersion: version, graph })

이 저장으로는 프레임 플랜, 채택, 엔드포인트 연결, 작업 기록, 보호된 휴지통 항목, 공유를 바꿀 수 없습니다. 이런 항목은 각각의 전용 동작을 사용하세요.

studio.generateKeyframe(id, input)

계획된 프레임을 채택하지 않고 생성합니다. 프레임에는 시험 실행이 없습니다.

generateKeyframe(id: string, input: { keyframeId: string; expectedRevision: number; clientRequestId?: string; overrides?: Record<string, unknown> }): Promise<{ data: { jobIds: string[]; deduped?: true; lane?: string } }>

속성

타입

const { data: caps } = await client.studio.capabilities()
const { data: { production } } = await client.studio.get(productionId, { detail: "full" })
const frame = production.keyframes?.[0]

if (frame && caps.operations.generateKeyframes) {
  const { data: generation } = await client.studio.generateKeyframe(productionId, {
    keyframeId: frame.id,
    expectedRevision: frame.revision,
    clientRequestId: crypto.randomUUID(),
  })
  // follow generation.jobIds with client.jobs, then call reconcile()
}

생성은 후보를 채택하지 않으며 캐릭터 초상화를 만들지도 않습니다. 설명만 있는 출연진 레퍼런스에는 초상화가 필요 없습니다.

studio.generateShot(id, input)

샷의 스틸이나 클립의 견적을 내거나 제출합니다.

generateShot(id: string, input: StudioShotGenerationInput): Promise<{ data: StudioGenerationReply }>

속성

타입

const { data: quote } = await client.studio.generateShot(productionId, { kind: "clip", shotId, dryRun: true })
if ("inputHash" in quote) {
  await client.studio.generateShot(productionId, {
    kind: "clip",
    shotId,
    expectedInputHash: quote.inputHash,
    clientRequestId: crypto.randomUUID(),
  })
}

연결된 클립. 계획된 프레임 사이를 잇는 클립의 견적에는 inputHash, 채택된 endpointPins, 정규화된 길이, 해상도, 화면 비율, 사운드 설정, 그리고 가격 계산에 쓰인 creditIdentifier가 들어 있습니다. 검토한 inputHash를 expectedInputHash로 전달하세요. 견적 이후 설정이나 채택된 프레임이 바뀌었다면, 호출은 아무것도 제출하기 전에 409 sequence_quote_changed로 실패합니다. 새 견적을 요청하세요. 견적의 크레딧은 추정치이며, 생성할 때는 현재 가격이 예약됩니다.

리테이크. operations.retakeLinkedClips가 true이면 kind: "clip"과 shotId와 함께 retakeResultKey를 전달하고, dryRun: true로 견적을 내세요. 검토한 expectedInputHash와 새 clientRequestId로 제출하고, mode, overrides, count는 빼세요. 리테이크는 플랜이나 채택이 바뀐 뒤에도 원래 요청과 보관된 프레임 이미지를 다시 사용합니다. 이전 테이크는 기록에 남습니다. 확인할 수 있는 원래 요청이나 보관된 이미지가 없는 테이크는 거부되며, 리테이크가 같은 동영상 바이트를 재현하지는 않습니다.

studio.acceptKeyframe(id, review, concurrency?)

계획된 프레임에 대해 검토한 후보를 채택합니다. 이는 별도의 명시적인 단계이며, 생성 과정에서 호출되는 일은 없습니다.

acceptKeyframe(id: string, review: StudioKeyframeAcceptanceInput, concurrency?: { baseVersion?: number; strict?: boolean; clientRequestId?: string })

속성

타입

await client.studio.acceptKeyframe(productionId, {
  keyframeId: frame.id,
  expectedRevision: frame.revision,
  resultKey,
  expectedAcceptedResultKey: frame.acceptedResultKey,
  requirementChecks, // one { requirementId, outcome } per requirement of the frame
})

충돌하면 일반적인 오류가 발생합니다. SDK는 다른 결과를 고르거나 최신 리비전으로 스스로 재시도하지 않습니다.

studio.reconcile(id)

후보를 채택하거나 생성을 시작하지 않고 완료된 작업을 기록합니다. 휴지통에 있는 장면의 작업도 확인합니다. 완료된 클립은 그 장면에 저장된 그래프에 남으며, restore_trashed로 복구할 수 있습니다.

reconcile(id: string): Promise<{ data: StudioProductionReply & { landed: string[]; pending: string[]; failed: string[]; version: number } }>

속성

타입

const { data } = await client.studio.reconcile(productionId)
console.log(data.landed, data.pending)

studio.setShared(id, input)

검토한 리비전에 묶어 프로덕션을 공유하거나 공유를 해제합니다. operations.revisionedSharing을 확인하고 expectedVersion을 전달하세요. 그러면 동시에 일어난 편집은 409 workflow_conflict로 실패하며, SDK는 재시도하지 않습니다. 공개 범위를 바꿀 수 있는 호출자만 사용할 수 있습니다.

setShared(id: string, input: { shared: boolean; allowEditableCopy?: boolean; expectedVersion?: number }): Promise<{ data: StudioProductionReply }>

속성

타입

await client.studio.setShared(productionId, { shared: true, allowEditableCopy: true, expectedVersion: version })

allowEditableCopy를 쓰면 소유자나 워크스페이스 관리자는 링크 열람자가 저장된 플랜, 프롬프트, 출연진 설명, 보관된 레퍼런스 입력, 테이크 기록을 복사할 수 있게 합니다. 휴지통과 비공개 검토 메모는 절대 포함되지 않습니다. 사본은 비공개이고 채택된 프레임이 없는 상태로 시작합니다. 복사를 끄거나 공유를 해제하면 새 사본을 만들 수 없으며, 이미 만든 사본은 독립적으로 남습니다.

studio.clone(id, input?)

저장된 프로덕션을 복사합니다. 연결된 프레임이 있는 프로덕션을 복사하기 전에 operations.cloneLinkedProductions를 확인하고, 불러온 expectedVersion을 전달하세요. 원본이 바뀌었다면 409로 실패합니다.

clone(id: string, input?: { name?: string; projectId?: string; expectedVersion?: number }): Promise<{ data: StudioProductionReply }>

속성

타입

const { data: { production: copy } } = await client.studio.clone(productionId, {
  name: "Rome chase copy",
  expectedVersion: version,
})

사본은 비공개로 시작합니다. 프레임 입력은 유지되며 내 스토리지 용량에 포함됩니다. 프레임, 장면, 시퀀스 id는 새로 발급되고, 실행 중인 작업과 채택된 프레임은 넘어가지 않습니다. 사본의 프레임에 의존하는 미디어를 생성하기 전에 그 프레임을 검토하고 채택하세요. 복사는 생성을 제출하지 않습니다.

studio.importBundle(input)

이식 가능한 프로덕션을 새 장면, 프레임, 시퀀스 id를 가진 새 비공개 프로덕션으로 가져옵니다(POST .../import-bundle). 미디어 없는 레시피와 플랜에는 operations.importPlannedBundles를, 보관된 프레임 미디어가 있는 번들에는 importLinkedBundles를 확인하세요.

importBundle(input: { bundle: Record<string, unknown>; projectId?: string }): Promise<{ data: StudioProductionReply }>

속성

타입

const { data: { production } } = await client.studio.importBundle({ bundle })

연결된 번들은 원본 프로덕션을 지정합니다. 서버는 내가 그 프로덕션을 소유하는지 확인하고, 무언가를 복사하기 전에 보관된 이미지를 모두 검증합니다. 접근 권한이 없거나 출처가 위조되었으면 새 프로덕션을 만들기 전에 가져오기가 거부됩니다. 어느 종류의 가져오기도 채택이나 실행 중인 작업을 가져오지 않으며, 미디어를 생성하지도 않습니다.

studio.appendBundle(id, input)

새 id와 정확한 리비전 확인을 거쳐 완전한 번들을 편집 가능한 프로덕션에 덧붙입니다(POST .../:id/import-bundle). OAuth 토큰에는 workflows:write가 필요하고, 나에게 프로덕션 편집 권한이 있어야 합니다. 먼저 appendPlannedBundles나 appendLinkedBundles를 확인하세요.

appendBundle(id: string, input: { bundle: Record<string, unknown>; expectedVersion: number; afterShotId?: string; applyFilm?: boolean }): Promise<{
  data: { production: StudioProductionRecord; importedShotIds: string[]; importedKeyframeIds: string[] }
}>

속성

타입

const { data } = await client.studio.appendBundle(productionId, { bundle, expectedVersion: version })
console.log(data.importedShotIds)

기존 장면, 프레임, 작업, 공유, 그 밖의 설정은 유지되고, 가져온 출연진 역할은 병합됩니다. 가져온 프레임은 다시 채택해야 합니다. 알 수 없는 afterShotId나 오래된 expectedVersion은 실패합니다.

client.shots

/s/:id 공유 링크 뒤에 있는 샷 레코드로, 공유와 리믹스에 쓰입니다. 샷은 빌더의 상태를 저장합니다. 피커 선택, 프롬프트, 대상 모델, @ 멘션 레퍼런스, 결과 URL이 추측할 수 없는 12자 id 아래에 저장되며, 이 id는 공유 키이기도 합니다. 샷은 기본적으로 비공개이며, 공유는 내가 직접 하는 공개 범위 변경입니다.

create(input?: CreateShotInput): Promise<{ id: string }>
get(id: string): Promise<{ shot: Shot }>
update(id: string, input: UpdateShotInput): Promise<{ shot: Shot }>
delete(id: string): Promise<void>

속성

타입

const { id } = await client.shots.create({ mode: "single", freeText: "A lighthouse in a storm", models: ["nano-banana-2"] })
await client.shots.update(id, { visibility: "public" }) // anyone with the id can now read it
const { shot } = await client.shots.get(id)

공개 샷은 id를 아는 누구나 읽을 수 있습니다. 비공개 샷은 소유자만 읽을 수 있고, 다른 사람은 NotFoundError를 받습니다. 샷을 수정하거나 삭제할 수 있는 사람은 소유자뿐입니다.

자주 묻는 질문

스튜디오 프로덕션이란 무엇인가요?

프로덕션은 설정에 영화의 샷을 담은 워크플로입니다. 각 샷에는 구도를 잡은 스틸, 선택 사항인 애니메이션 클립, 그리고 이를 만든 플랜, 룩, 출연진, 보이스가 있습니다. 스튜디오 앱과 SDK는 같은 프로덕션을 읽고 씁니다.

client.studio에 메서드 세트가 두 개인 이유는 무엇인가요?

client.studio.productions는 프로덕션 문서를 다룹니다. 오퍼레이션, 스틸, 클립, 보이스, 음악을 처리하고 페이로드 자체를 반환합니다. client.studio는 계획된 프레임과 그 검토를 다루고, API의 data 래퍼를 반환합니다. 둘 다 같은 프로덕션에 접근합니다.

생성을 재시도할 때 두 번 결제하지 않으려면 어떻게 하나요?

직접 만든 clientRequestId를 전달하고, 재시도할 때 같은 값을 다시 쓰세요. 서버는 첫 호출이 시작한 작업으로 응답하고, 응답에 deduped를 표시하며, 다시 요금을 부과하지 않습니다.

스틸이나 클립의 가격을 먼저 확인하려면 어떻게 하나요?

dryRun을 true로 설정해 generateStill이나 generateClip을 호출하세요. 응답은 크레딧이 담긴 견적이며, 아무것도 제출되지 않습니다. isStudioGenerateEstimate로 응답을 구분하세요.

최종 업데이트

목차