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 library —
Collection: My Series → Episode 03 → 12 assets - Idempotency — repeated
syncwith the sameidempotencyKeyreturns the first response (24h window). Network blips don’t double-charge. - Double dedup —
source_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.syncwritessys_audit_logs. Admins can replay third-party activity throughGET /v1/admin/collections/audit-logs.
Prerequisites
- A Picora account on Pro or Pro+ plan (
collection_count_limit ≥ 50) - 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
- API Key with scopes
- Node.js 18+ (for the JS SDK; Python SDK is on the roadmap)
Install the SDK
npm install @picora/sdk# orpnpm add @picora/sdkQuickstart — 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 episodeconst 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 flowconst 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 deviceconsole.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 clientconst 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[]:
| status | reason | What happened |
|---|---|---|
applied | — | First time this asset hits this episode. Written to collection_episode_assets + audit row inserted. |
skipped_duplicate | source_hash_matched | Your sourceHash already exists on this episode. No write, no audit churn. |
skipped_duplicate | natural_key_matched | Same (episode, resource_type, resource_id) already linked. No write. |
failed | resource_not_found | The resourceId doesn’t exist in your account (deleted? wrong ID?). |
failed | forbidden | The resource belongs to someone else. Cross-tenant push rejected. |
Failure is per-item, not per-batch — one bad asset never aborts the others.
Recommended source_hash
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)
| Capability | Status |
|---|---|
| Python SDK | Planned (picora-sdk on PyPI; same shape as JS) |
TUS multipart upload baked into episodes.sync | Planned (large file streaming) |
| Real-time webhooks (sync.completed events) | Planned (v0.65+) |
findOrCreate helper on collections / episodes | Workaround: 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_xxxFilter by 1h / 24h / 7d window in the admin UI under /admin/collection-audit. Each row shows the resource IDs touched + the metadata you sent.
Related
- API reference — full OpenAPI spec
- MCP — different integration shape for AI assistants (Claude / Cursor / Moraya)
- Mobile sync — phone photos via iOS Shortcuts (no SDK)