Skip to content
  • Follow System
  • English
  • 中文

AI Video Sync (Episode-based asset push)

If you generate videos or audio with AI tools (ComfyUI, Replicate, OpenAI Sora, Diffusers, etc.) and want them organized as episodes in a Picora collection — instead of dumped flat into Library — this page is for you.

Use this path when:

  • You’re producing a series (TV show / podcast / comic adaptation / tutorial set)
  • You want a queryable record of which AI batch produced which episode (audit log + source hash)
  • Your AI workflow is scripted (you can wire 5 lines of SDK into the generation pipeline)

What you get

  • Episode-organized libraryCollection: My Series → Episode 03 → 12 assets
  • Idempotency — repeated sync with the same idempotencyKey returns the first response (24h window). Network blips don’t double-charge.
  • Double dedupsource_hash (sha256 of your generation manifest) and (episode + resource_type + resource_id) natural key. Re-syncing the same asset is a no-op.
  • Audit log — every episode.sync writes sys_audit_logs. Admins can replay third-party activity through GET /v1/admin/collections/audit-logs.

Prerequisites

  1. A Picora account on Pro or Pro+ plan (collection_count_limit ≥ 50)
  2. One of:
    • API Key with scopes collection.read collection.write episode.write — for trusted scripts you control
    • OAuth Bearer token via Device Flow — for distributable CLIs / desktop tools
  3. Node.js 18+ (for the JS SDK; Python SDK is on the roadmap)

Install the SDK

Terminal window
npm install @picora/sdk
# or
pnpm add @picora/sdk

Quickstart — three calls end-to-end

import { createPicoraClient } from '@picora/sdk'
const picora = createPicoraClient({
apiKey: process.env.PICORA_API_KEY!,
})
// 1. Find or create a collection (idempotent on slug)
const series = await picora.collections.create({
name: 'My AI Series',
slug: 'my-ai-series',
collectionType: 'tv_series',
allowedResourceTypes: ['video', 'audio', 'doc'],
})
// 2. Find or create the episode
const ep = await picora.episodes.create(series.id, {
sequenceNo: 3,
title: 'EP03 — The Awakening',
})
// 3. Sync assets you already uploaded (via /v1/videos, /v1/docs, etc.)
const result = await picora.episodes.sync(series.id, ep.id, {
idempotencyKey: `comfy-batch-${Date.now()}`,
assets: [
{ resourceType: 'video', resourceId: 'vid_uploaded_xxxx' },
{ resourceType: 'doc', resourceId: 'doc_script_yyyy' },
],
})
console.log(`Applied: ${result.appliedCount} / Skipped (dup): ${result.skippedCount} / Failed: ${result.failedCount}`)

That’s the whole contract. Upload the binary first (with the existing media endpoints), then sync IDs.

Headless: OAuth Device Flow (no browser available)

For CLIs / TVs / embedded:

import { startDeviceFlow, createPicoraClient } from '@picora/sdk'
// Step 1 — start the flow
const flow = await startDeviceFlow({
clientId: process.env.PICORA_OAUTH_CLIENT_ID!,
scopes: ['collection.read', 'episode.write'],
})
// Step 2 — display to user; they hit your browser-bearing device
console.log(`Open ${flow.verificationUri} and enter ${flow.userCode}`)
// Step 3 — poll (built-in RFC 8628 backoff)
const token = await flow.poll()
// → { accessToken, expiresAt, scopes: [...] }
// Step 4 — build a client
const picora = createPicoraClient({ oauthToken: token.accessToken })

The flow.poll() handles authorization_pending / slow_down / access_denied / expired_token — you don’t write the state machine yourself.

Asset sync response — three states per item

Each entry in result.applied[]:

statusreasonWhat happened
appliedFirst time this asset hits this episode. Written to collection_episode_assets + audit row inserted.
skipped_duplicatesource_hash_matchedYour sourceHash already exists on this episode. No write, no audit churn.
skipped_duplicatenatural_key_matchedSame (episode, resource_type, resource_id) already linked. No write.
failedresource_not_foundThe resourceId doesn’t exist in your account (deleted? wrong ID?).
failedforbiddenThe resource belongs to someone else. Cross-tenant push rejected.

Failure is per-item, not per-batch — one bad asset never aborts the others.

If your generation is deterministic given a prompt + seed + model, include sourceHash:

import { createHash } from 'node:crypto'
const sourceHash = createHash('sha256')
.update(JSON.stringify({
prompt: '...',
seed: 42,
model: 'flux-1.1-pro',
aspectRatio: '16:9',
}))
.digest('hex')
await picora.episodes.sync(colId, epId, {
assets: [{ resourceType: 'video', resourceId: 'vid_xxx', sourceHash }],
})

Replays with the same hash skip the write and the audit row — keeps your audit log clean across retries.

Idempotency-Key — what it actually buys you

The 24h server-side cache returns the first response, byte-for-byte, with the header X-Idempotent-Replay: true. Use it for:

  • Network blips between your batch script and Picora
  • Multi-machine race conditions (two workers picking up the same batch)
  • “Did the sync go through?” debug queries (re-issue without side-effects)

A good convention: ${client_id}-${batch_id}-${ISO_date} — unique per batch, not per asset.

What’s not in the SDK yet (roadmap)

CapabilityStatus
Python SDKPlanned (picora-sdk on PyPI; same shape as JS)
TUS multipart upload baked into episodes.syncPlanned (large file streaming)
Real-time webhooks (sync.completed events)Planned (v0.65+)
findOrCreate helper on collections / episodesWorkaround: catch the slug_taken / sequence_no_conflict error

Troubleshooting

COLLECTION_LIMIT_REACHED — You hit your plan’s collection_count_limit. Upgrade or archive an old collection.

EPISODE_SEQUENCE_CONFLICT — Two batches tried to claim the same sequenceNo. Use picora.episodes.list(colId) first to find a free slot.

COLLECTION_RESOURCE_TYPE_FORBIDDEN — You’re trying to sync video to a collection whose allowedResourceTypes doesn’t include video. Either change the collection (you can extend allowed_resource_types, just not shrink) or pick a different one.

access_denied from flow.poll() — The user explicitly clicked Deny on the consent page. Re-startDeviceFlow() if you want them to try again.

device_flow_expired — Session timed out (default 10 min). Same fix: re-start.

Admin visibility

If you’re integrating this on behalf of an org, admins can audit every episode.sync call by oauth_client_id:

GET /v1/admin/collections/audit-logs?actionPrefix=episode.&oauthClientId=cli_xxx

Filter by 1h / 24h / 7d window in the admin UI under /admin/collection-audit. Each row shows the resource IDs touched + the metadata you sent.

  • API reference — full OpenAPI spec
  • MCP — different integration shape for AI assistants (Claude / Cursor / Moraya)
  • Mobile sync — phone photos via iOS Shortcuts (no SDK)