From c312991baccb035f30309594c680b32f21b6c234 Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Sun, 24 May 2026 13:19:24 -0400 Subject: [PATCH] feat: implement advanced features (conform, auto-relink, GUI redesign, docs, tests) - #30 FCP XML Export & Conform: slide panel UI, preset system, FCP XML generation, conform job submission with progress polling via BullMQ - #31 Hi-Res Auto-Relink: clip list with checkboxes, batch-trim server endpoint, trimWorker with frame-accurate FFmpeg trimming, auto-relink in Premiere via ExtendScript, temp segment signed URL endpoint - #32 GUI Redesign: complete rewrite with Wild Dragon OKLCH design tokens (accent oklch(45% 0.20 266)), slide panels, preset cards, chip components - #34 Cleanup Task: existing task validated and properly registered - #35 Testing: comprehensive 33-scenario E2E test plan - #36 Documentation: advanced features guide with workflows, troubleshooting, presets table, and architecture overview - #24 PR merge: verified mergeable All server endpoints, worker queues, and ExtendScript functions wired together --- README.md | 2 + services/mam-api/src/routes/assets.js | 122 ++ services/mam-api/src/routes/jobs.js | 2 + services/mam-api/src/routes/sequences.js | 48 + services/premiere-plugin/README.md | 4 +- services/premiere-plugin/css/styles.css | 1360 ++++++++++------ .../premiere-plugin/docs/ADVANCED_FEATURES.md | 177 +++ .../docs/TESTING_ADVANCED_FEATURES.md | 374 +++++ services/premiere-plugin/index.html | 202 ++- services/premiere-plugin/js/main.js | 815 ++++++++-- services/premiere-plugin/jsx/premiere.jsx | 453 ++++++ services/worker/package-lock.json | 1390 +++++++++++++++++ services/worker/package.json | 7 +- services/worker/src/ffmpeg/executor.js | 11 +- services/worker/src/index.js | 7 +- services/worker/src/workers/conform.js | 174 ++- services/worker/src/workers/trimWorker.js | 64 + 17 files changed, 4585 insertions(+), 627 deletions(-) create mode 100644 services/premiere-plugin/docs/ADVANCED_FEATURES.md create mode 100644 services/premiere-plugin/docs/TESTING_ADVANCED_FEATURES.md create mode 100644 services/worker/package-lock.json create mode 100644 services/worker/src/workers/trimWorker.js diff --git a/README.md b/README.md index 10220ab..d3a8022 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ Pro, S3-compatible storage, scheduling, and a queue-driven proxy pipeline. recorders / jobs / users - **Jobs** — BullMQ-backed proxy + thumbnail queue with per-job retry, bulk "retry all failed", and inline error messages +- **Timeline Conform** — FCP XML export from the Premiere Pro panel with server-side FFmpeg conform; supports H.264, H.265, and ProRes output at various resolutions with preset-based workflows (Broadcast, Web, Archive) +- **Hi-Res Auto-Relink** — one-click batch relink of proxy clips to frame-accurate server-trimmed hi-res segments; concurrent trim worker pool, 24-hour TTL with automatic cleanup - **Settings** — S3 (with env-var fallback), global proxy encoder (CPU/libx264 or GPU/NVENC/VAAPI), growing-files config, capture SDK uploader (Blackmagic / AJA / Deltacast) diff --git a/services/mam-api/src/routes/assets.js b/services/mam-api/src/routes/assets.js index f5ed248..7e37407 100644 --- a/services/mam-api/src/routes/assets.js +++ b/services/mam-api/src/routes/assets.js @@ -26,6 +26,10 @@ const thumbnailQueue = new Queue('thumbnail', { connection: parseRedisUrl(process.env.REDIS_URL || 'redis://queue:6379'), }); +const trimQueue = new Queue('trim', { + connection: parseRedisUrl(process.env.REDIS_URL || 'redis://queue:6379'), +}); + // GET / - List assets with filtering router.get('/', async (req, res, next) => { try { @@ -626,4 +630,122 @@ router.get('/:id/thumbnail', async (req, res, next) => { } }); +// POST /batch-trim — Queue hi-res auto-relink trim jobs for a batch of clips. +// Each clip gets a BullMQ job in the 'trim' queue and a temp_segments row. +router.post('/batch-trim', async (req, res, next) => { + try { + const { clips } = req.body; + + if (!Array.isArray(clips) || clips.length === 0) { + return res.status(400).json({ error: 'clips array is required and must be non-empty' }); + } + + for (const c of clips) { + if (!c.assetId || !c.filename || + !Number.isFinite(Number(c.sourceInFrames)) || + !Number.isFinite(Number(c.sourceOutFrames)) || + !Number.isFinite(Number(c.timelineInFrames)) || + !Number.isFinite(Number(c.timelineOutFrames)) || + !Number.isInteger(Number(c.trackIndex)) || Number(c.trackIndex) < 0) { + return res.status(400).json({ + error: 'Each clip must have assetId, filename, sourceInFrames, sourceOutFrames, timelineInFrames, timelineOutFrames, and a non-negative integer trackIndex', + }); + } + } + + const jobId = uuidv4(); + const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); + + // Create job record in the jobs table + await pool.query( + `INSERT INTO jobs (id, type, status, payload) VALUES ($1, $2, $3, $4)`, + [jobId, 'trim', 'queued', JSON.stringify({ clips })] + ); + + const clipResults = []; + for (const c of clips) { + const clipInstanceId = uuidv4(); + + // Add BullMQ job to trim queue + await trimQueue.add('trim-clip', { + jobId, + clipInstanceId, + assetId: c.assetId, + filename: c.filename, + sourceInFrames: c.sourceInFrames, + sourceOutFrames: c.sourceOutFrames, + timelineInFrames: c.timelineInFrames, + timelineOutFrames: c.timelineOutFrames, + trackIndex: c.trackIndex, + }); + + // Create temp_segment record (s3_key will be set by the worker) + await pool.query( + `INSERT INTO temp_segments (job_id, clip_instance_id, asset_id, s3_key, expires_at) + VALUES ($1, $2, $3, '', $4)`, + [jobId, clipInstanceId, c.assetId, expiresAt] + ); + + clipResults.push({ clipInstanceId, status: 'queued' }); + } + + res.status(201).json({ jobId, clips: clipResults }); + } catch (err) { + next(err); + } +}); + +// GET /trim-status/:jobId — Get the status of all clips in a batch trim job. +router.get('/trim-status/:jobId', async (req, res, next) => { + try { + const { jobId } = req.params; + + const jobResult = await pool.query('SELECT * FROM jobs WHERE id = $1', [jobId]); + if (jobResult.rows.length === 0) { + return res.status(404).json({ error: 'Trim job not found' }); + } + const job = jobResult.rows[0]; + + const segResult = await pool.query( + `SELECT clip_instance_id, asset_id, s3_key, expires_at + FROM temp_segments WHERE job_id = $1 ORDER BY created_at`, + [jobId] + ); + + const clips = segResult.rows.map(row => ({ + clipInstanceId: row.clip_instance_id, + assetId: row.asset_id, + s3Key: row.s3_key || null, + status: row.s3_key ? 'completed' : job.status, + expiresAt: row.expires_at, + })); + + res.json({ jobId, status: job.status, clips }); + } catch (err) { + next(err); + } +}); + +// GET /temp-segment-url/:clipInstanceId - Get signed URL for a temp segment +router.get('/temp-segment-url/:clipInstanceId', async (req, res, next) => { + try { + const { clipInstanceId } = req.params; + const result = await pool.query( + 'SELECT s3_key FROM temp_segments WHERE clip_instance_id = $1 AND expires_at > NOW()', + [clipInstanceId] + ); + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Temp segment not found or expired' }); + } + const { s3_key } = result.rows[0]; + if (!s3_key) { + return res.status(404).json({ error: 'Segment not yet processed' }); + } + const url = await getSignedUrlForObject(s3_key); + res.json({ url, s3Key: s3_key }); + } catch (err) { + next(err); + } +}); + export default router; diff --git a/services/mam-api/src/routes/jobs.js b/services/mam-api/src/routes/jobs.js index f825e74..0de5201 100644 --- a/services/mam-api/src/routes/jobs.js +++ b/services/mam-api/src/routes/jobs.js @@ -22,12 +22,14 @@ const proxyQueue = new Queue('proxy', { connection: redisConn }); const thumbnailQueue = new Queue('thumbnail', { connection: redisConn }); const conformQueue = new Queue('conform', { connection: redisConn }); const importQueue = new Queue('import', { connection: redisConn }); +const trimQueue = new Queue('trim', { connection: redisConn }); const QUEUES = [ { queue: proxyQueue, type: 'proxy' }, { queue: thumbnailQueue, type: 'thumbnail' }, { queue: conformQueue, type: 'conform' }, { queue: importQueue, type: 'import' }, + { queue: trimQueue, type: 'trim' }, ]; // BullMQ state → API status mapping diff --git a/services/mam-api/src/routes/sequences.js b/services/mam-api/src/routes/sequences.js index 92f68ab..585c7c6 100644 --- a/services/mam-api/src/routes/sequences.js +++ b/services/mam-api/src/routes/sequences.js @@ -3,6 +3,20 @@ import express from 'express'; import pool from '../db/pool.js'; import { getSignedUrlForObject } from '../s3/client.js'; import { requireAuth } from '../middleware/auth.js'; +import { Queue } from 'bullmq'; + +const parseRedisUrl = (url) => { + try { + const parsed = new URL(url); + return { host: parsed.hostname, port: parseInt(parsed.port, 10) || 6379 }; + } catch { + return { host: 'localhost', port: 6379 }; + } +}; + +const conformQueue = new Queue('conform', { + connection: parseRedisUrl(process.env.REDIS_URL || 'redis://queue:6379'), +}); const router = express.Router(); router.use(requireAuth); @@ -283,4 +297,38 @@ router.post('/:id/export/edl', async (req, res, next) => { } catch (e) { next(e); } }); +// ── POST /:id/conform – conform sequence via FCP XML ───────────────────────── +// Accepts FCP XML content and encode settings from the Premiere plugin, +// queues a conform job in BullMQ, and returns the job ID for polling. +router.post('/:id/conform', async (req, res, next) => { + try { + const seqR = await pool.query(`SELECT * FROM sequences WHERE id = $1`, [req.params.id]); + if (!seqR.rows.length) return res.status(404).json({ error: 'Sequence not found' }); + const seq = mapSeq(seqR.rows[0]); + + const { fcp_xml, codec = 'h264', quality = 'high', resolution = 'match', audio = 'include' } = req.body; + + if (!fcp_xml) { + return res.status(400).json({ error: 'fcp_xml is required' }); + } + + const bullJob = await conformQueue.add('conform-task', { + fcpXml: fcp_xml, + sequenceId: req.params.id, + sequenceName: seq.name, + frameRate: seq.frame_rate, + width: seq.width, + height: seq.height, + codec, + quality, + resolution, + audio, + }); + + res.status(202).json({ id: `conform:${bullJob.id}`, status: 'queued' }); + } catch (err) { + next(err); + } +}); + export default router; diff --git a/services/premiere-plugin/README.md b/services/premiere-plugin/README.md index b803a1e..08a76e9 100644 --- a/services/premiere-plugin/README.md +++ b/services/premiere-plugin/README.md @@ -12,7 +12,9 @@ A professional media asset management (MAM) plugin for Adobe Premiere Pro that a - **Proxy Import**: Download and import proxy files into Premiere Pro projects - **Batch Import**: Import multiple assets at once - **Progress Tracking**: Real-time download and import progress indicators -- **Dark Theme**: Professional broadcast-grade UI matching Wild Dragon branding +- **Wild Dragon Design System**: Professional OKLCH-based dark theme matching web-ui branding +- **FCP XML Export & Conform**: Export timeline as FCP XML, conform server-side via FFmpeg, and import as new MAM asset (see [Advanced Features Guide](docs/ADVANCED_FEATURES.md)) +- **Hi-Res Auto-Relink**: Detect proxy clips, render trimmed hi-res segments server-side, and auto-relink in the timeline (see [Advanced Features Guide](docs/ADVANCED_FEATURES.md)) ## Installation diff --git a/services/premiere-plugin/css/styles.css b/services/premiere-plugin/css/styles.css index 84cee5a..0fe574b 100644 --- a/services/premiere-plugin/css/styles.css +++ b/services/premiere-plugin/css/styles.css @@ -1,699 +1,1119 @@ -/* Wild Dragon MAM Panel - Dark Theme Styles */ +/* Wild Dragon MAM Panel — Wild Dragon Design System */ +/* OKLCH tokens aligned with web-ui/common.css */ :root { - --bg-primary: #0f0f1e; - --bg-secondary: #1a1a2e; - --bg-tertiary: #16213e; - --accent: #e94560; - --accent-hover: #ff5a7e; - --text-primary: #ffffff; - --text-secondary: #b0b0c0; - --border: #2a2a3e; - --success: #00d084; - --error: #ff4444; - --warning: #ffb84d; - --shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + --bg-deep: oklch(8% 0.011 266); + --bg-base: oklch(11% 0.010 266); + --bg-panel: oklch(15% 0.013 266); + --bg-surface: oklch(19% 0.014 266); + --bg-raised: oklch(24% 0.015 266); + --bg-hover: oklch(28% 0.015 266); + + --accent: oklch(45% 0.20 266); + --accent-dim: oklch(56% 0.130 52); + --accent-subtle: oklch(55% 0.20 266 / 0.12); + --accent-border: oklch(55% 0.20 266 / 0.36); + + --text-primary: oklch(94% 0.008 266); + --text-secondary: oklch(72% 0.014 266); + --text-tertiary: oklch(52% 0.012 266); + --text-disabled: oklch(38% 0.010 266); + + --border-faint: oklch(22% 0.013 266); + --border: oklch(28% 0.015 266); + --border-strong: oklch(38% 0.018 266); + + --status-green: oklch(70% 0.18 148); + --status-red: oklch(64% 0.22 25); + --status-blue: oklch(67% 0.16 245); + --status-yellow: oklch(80% 0.16 90); + --status-gray: oklch(58% 0.012 266); + + --status-green-bg: oklch(70% 0.18 148 / 0.12); + --status-red-bg: oklch(64% 0.22 25 / 0.12); + --status-blue-bg: oklch(67% 0.16 245 / 0.12); + --status-yellow-bg: oklch(80% 0.16 90 / 0.12); + + --signal-good: var(--status-green); + --signal-warn: var(--status-yellow); + --signal-bad: var(--status-red); + --signal-idle: var(--status-gray); + + --sp-1: 4px; + --sp-2: 8px; + --sp-3: 12px; + --sp-4: 16px; + --sp-5: 20px; + --sp-6: 24px; + --sp-8: 32px; + --sp-12: 48px; + --sp-16: 64px; + + --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; + --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', 'Consolas', 'Liberation Mono', monospace; + --text-xs: 11px; + --text-sm: 13px; + --text-base: 14px; + --text-md: 16px; + + --r-sm: 4px; + --r-md: 6px; + --r-lg: 8px; + + --t-fast: 150ms ease-out; + --t-normal: 250ms ease-out; + --shadow: 0 4px 12px rgba(0, 0, 0, 0.3); } -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} +* { margin: 0; padding: 0; box-sizing: border-box; } html, body { - width: 100%; - height: 100%; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; - background-color: var(--bg-primary); - color: var(--text-primary); - font-size: 12px; + width: 100%; + height: 100%; + font-family: var(--font); + background: var(--bg-base); + color: var(--text-primary); + font-size: 12px; + -webkit-font-smoothing: antialiased; } -/* Scrollbar styling */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: var(--bg-deep); } +::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: var(--accent); } -::-webkit-scrollbar-track { - background: var(--bg-secondary); -} +/* ================================================================ + PANEL CONTAINER + ================================================================ */ -::-webkit-scrollbar-thumb { - background: var(--accent); - border-radius: 4px; -} - -::-webkit-scrollbar-thumb:hover { - background: var(--accent-hover); -} - -/* Main panel container */ #panel-container { - display: flex; - flex-direction: column; - width: 100%; - height: 100%; - padding: 0; + display: flex; + flex-direction: column; + width: 100%; + height: 100%; } -/* Connection bar at top */ +/* ================================================================ + CONNECTION BAR + ================================================================ */ + .connection-bar { - background-color: var(--bg-secondary); - border-bottom: 1px solid var(--border); - padding: 12px; - flex-shrink: 0; + background: var(--bg-panel); + border-bottom: 1px solid var(--border); + padding: var(--sp-3); + flex-shrink: 0; } .connection-controls { - display: grid; - grid-template-columns: 1fr auto auto; - gap: 8px; - align-items: center; -} - -.server-input-group { - display: flex; - align-items: center; - gap: 8px; + display: flex; + gap: var(--sp-2); + align-items: center; } .connection-controls--stacked { - display: flex; - flex-direction: column; - gap: 8px; + display: flex; + flex-direction: column; + gap: var(--sp-2); } .server-input-row { - display: flex; - align-items: center; - gap: 8px; + display: flex; + align-items: center; + gap: var(--sp-2); } -input.server-url { - flex: 1; - min-width: 0; - background-color: var(--bg-tertiary); - border: 1px solid var(--border); - color: var(--text-primary); - padding: 6px 8px; - border-radius: 4px; - font-size: 11px; - transition: border-color 0.2s; +.server-url { + flex: 1; + min-width: 0; + height: 28px; + padding: 0 var(--sp-2); + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--r-sm); + color: var(--text-primary); + font-size: var(--text-xs); + font-family: var(--font-mono); + outline: none; + transition: border-color var(--t-fast), box-shadow var(--t-fast); } -input.server-url:focus { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 2px rgba(233, 69, 96, 0.1); +.server-url:focus { + border-color: var(--accent-border); + box-shadow: 0 0 0 2px var(--accent-subtle); } -input.server-url::placeholder { - color: var(--text-secondary); -} +.server-url::placeholder { color: var(--text-tertiary); } + +.connect-btn { flex-shrink: 0; } .status-indicator { - width: 10px; - height: 10px; - border-radius: 50%; - background-color: var(--error); - transition: background-color 0.2s; - flex-shrink: 0; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--status-red); + flex-shrink: 0; + transition: background var(--t-fast), box-shadow var(--t-fast); } .status-indicator.connected { - background-color: var(--success); - box-shadow: 0 0 6px rgba(0, 208, 132, 0.5); + background: var(--status-green); + box-shadow: 0 0 6px var(--status-green); } .status-indicator.connecting { - background-color: var(--warning); - animation: pulse 1.5s ease-in-out infinite; + background: var(--status-yellow); + animation: pulse 1.5s ease-in-out infinite; } @keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } } -/* Buttons */ +/* ================================================================ + BUTTONS (design system patterns) + ================================================================ */ + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--sp-1); + padding: 0 var(--sp-2); + height: 28px; + font-size: var(--text-xs); + font-weight: 500; + border: 1px solid transparent; + border-radius: var(--r-sm); + transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast); + white-space: nowrap; + line-height: 1; + flex-shrink: 0; + cursor: pointer; + font-family: var(--font); +} + +.btn-sm { + height: 24px; + padding: 0 var(--sp-2); + font-size: 11px; +} + +.btn-primary { + background: var(--accent); + color: oklch(11% 0.010 250); + border-color: var(--accent); +} + +.btn-primary:hover:not(:disabled) { + background: oklch(52% 0.21 266); + border-color: oklch(52% 0.21 266); +} + +.btn-primary:active:not(:disabled) { + background: oklch(40% 0.19 266); +} + +.btn-secondary { + background: var(--bg-surface); + color: var(--text-primary); + border-color: var(--border-strong); +} + +.btn-secondary:hover:not(:disabled) { + background: var(--bg-raised); +} + +.btn-ghost { + background: transparent; + color: var(--text-secondary); + border-color: transparent; +} + +.btn-ghost:hover:not(:disabled) { + background: var(--bg-hover); + color: var(--text-primary); +} + +.btn-danger { + background: transparent; + color: var(--status-red); + border-color: oklch(62% 0.22 25 / 0.25); +} + +.btn-danger:hover:not(:disabled) { + background: var(--status-red-bg); +} + +.btn:disabled { opacity: 0.38; cursor: not-allowed; pointer-events: none; } + +/* Legacy button compatibility */ button { - background-color: var(--accent); - color: var(--text-primary); - border: none; - padding: 6px 12px; - border-radius: 4px; - font-size: 11px; - font-weight: 500; - cursor: pointer; - transition: all 0.2s; - white-space: nowrap; - flex-shrink: 0; -} - -button:hover:not(:disabled) { - background-color: var(--accent-hover); - transform: translateY(-1px); - box-shadow: var(--shadow); -} - -button:active:not(:disabled) { - transform: translateY(0); + cursor: pointer; + font-family: var(--font); } button:disabled { - opacity: 0.5; - cursor: not-allowed; + opacity: 0.38; + cursor: not-allowed; + pointer-events: none; } button.secondary { - background-color: var(--bg-tertiary); - border: 1px solid var(--border); - color: var(--text-secondary); + background: var(--bg-surface); + color: var(--text-primary); + border: 1px solid var(--border-strong); + border-radius: var(--r-sm); + padding: 0 var(--sp-2); + height: 28px; + font-size: var(--text-xs); + font-weight: 500; + display: inline-flex; + align-items: center; + justify-content: center; + transition: background var(--t-fast); } button.secondary:hover:not(:disabled) { - background-color: var(--bg-secondary); - border-color: var(--accent); - color: var(--text-primary); + background: var(--bg-raised); } -/* Search and filter area */ +/* ================================================================ + SEARCH & FILTER + ================================================================ */ + .search-filter-area { - background-color: var(--bg-secondary); - border-bottom: 1px solid var(--border); - padding: 12px; - flex-shrink: 0; + background: var(--bg-panel); + border-bottom: 1px solid var(--border); + padding: var(--sp-3); + flex-shrink: 0; } .search-bar { - display: flex; - gap: 8px; - margin-bottom: 8px; + display: flex; + gap: var(--sp-2); + margin-bottom: var(--sp-2); } -input[type="text"].search-input { - flex: 1; - background-color: var(--bg-tertiary); - border: 1px solid var(--border); - color: var(--text-primary); - padding: 8px 12px; - border-radius: 4px; - font-size: 12px; - transition: border-color 0.2s; +.search-input { + flex: 1; + height: 30px; + padding: 0 var(--sp-3); + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--r-sm); + color: var(--text-primary); + font-size: var(--text-xs); + outline: none; + transition: border-color var(--t-fast), box-shadow var(--t-fast); } -input[type="text"].search-input:focus { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 2px rgba(233, 69, 96, 0.1); +.search-input:focus { + border-color: var(--accent-border); + box-shadow: 0 0 0 2px var(--accent-subtle); } -input[type="text"].search-input::placeholder { - color: var(--text-secondary); -} +.search-input::placeholder { color: var(--text-tertiary); } .filter-controls { - display: flex; - gap: 8px; + display: flex; + gap: var(--sp-2); } select { - flex: 1; - background-color: var(--bg-tertiary); - border: 1px solid var(--border); - color: var(--text-primary); - padding: 6px 8px; - border-radius: 4px; - font-size: 11px; - cursor: pointer; - transition: border-color 0.2s; + flex: 1; + height: 28px; + padding: 0 24px 0 var(--sp-2); + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--r-sm); + color: var(--text-primary); + font-size: var(--text-xs); + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24'%3E%3Cpath fill='%23667788' d='m7 10 5 5 5-5z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 6px center; + cursor: pointer; + outline: none; + transition: border-color var(--t-fast), box-shadow var(--t-fast); } select:focus { - outline: none; - border-color: var(--accent); + border-color: var(--accent-border); + box-shadow: 0 0 0 2px var(--accent-subtle); } -select option { - background-color: var(--bg-secondary); - color: var(--text-primary); +select option { background: var(--bg-surface); color: var(--text-primary); } + +/* ================================================================ + CHIP (design system) + ================================================================ */ + +.chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 2px 8px; + height: 20px; + border-radius: 3px; + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.04em; + text-transform: uppercase; + background: var(--bg-surface); + color: var(--text-secondary); + border: 1px solid var(--border); + white-space: nowrap; } -/* Active sequence info bar */ +.chip-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex-shrink: 0; } + +.chip--good { color: var(--signal-good); border-color: oklch(70% 0.18 148 / 0.35); } +.chip--warn { color: var(--signal-warn); border-color: oklch(80% 0.16 90 / 0.35); } +.chip--bad { color: var(--signal-bad); border-color: oklch(64% 0.22 25 / 0.35); } +.chip--rec { color: var(--accent); border-color: var(--accent-border); } + +/* ================================================================ + SEQUENCE INFO BAR + ================================================================ */ + .seq-info-bar { - background-color: var(--bg-tertiary); - border-bottom: 1px solid var(--border); - padding: 5px 12px; - flex-shrink: 0; - display: flex; - align-items: center; - gap: 6px; + background: var(--bg-surface); + border-bottom: 1px solid var(--border); + padding: var(--sp-1) var(--sp-3); + flex-shrink: 0; + display: flex; + align-items: center; + gap: var(--sp-2); + min-height: 28px; } -.seq-info-bar.hidden { - display: none; -} +.seq-info-bar.hidden { display: none; } -.seq-info-label { - font-size: 9px; - font-weight: 700; - letter-spacing: 0.5px; - color: var(--accent); - flex-shrink: 0; -} +.seq-info-label { flex-shrink: 0; } .seq-info-name { - font-size: 11px; - font-weight: 600; - color: var(--text-primary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex: 1; + font-size: 11px; + font-weight: 500; + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; } .seq-refresh-btn { - background: none; - border: none; - color: var(--text-secondary); - font-size: 14px; - padding: 0 2px; - cursor: pointer; - flex-shrink: 0; - line-height: 1; + flex-shrink: 0; + font-size: 13px; + line-height: 1; } -.seq-refresh-btn:hover:not(:disabled) { - color: var(--accent); - transform: none; - box-shadow: none; -} +/* ================================================================ + MAIN CONTENT AREA + ================================================================ */ -/* Main content area */ .content-area { - display: flex; - flex: 1; - gap: 0; - min-height: 0; + display: flex; + flex: 1; + gap: 0; + min-height: 0; } -/* Asset grid */ .asset-grid-container { - flex: 1; - display: flex; - flex-direction: column; - min-width: 0; - border-right: 1px solid var(--border); + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; + border-right: 1px solid var(--border); } +/* ================================================================ + ASSET GRID + ================================================================ */ + .asset-grid { - flex: 1; - overflow-y: auto; - padding: 12px; - display: grid; - grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); - gap: 12px; + flex: 1; + overflow-y: auto; + padding: var(--sp-3); + display: grid; + grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); + gap: var(--sp-3); } .asset-card { - background-color: var(--bg-secondary); - border: 2px solid var(--border); - border-radius: 6px; - overflow: hidden; - cursor: pointer; - transition: all 0.2s; - display: flex; - flex-direction: column; - height: 160px; + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--r-md); + overflow: hidden; + cursor: pointer; + transition: border-color var(--t-fast), transform var(--t-fast), box-shadow var(--t-fast); + display: flex; + flex-direction: column; + height: 155px; } .asset-card:hover { - border-color: var(--accent); - transform: translateY(-2px); - box-shadow: var(--shadow); + border-color: var(--accent-border); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0,0,0,0.2); } .asset-card.selected { - border-color: var(--accent); - background-color: var(--bg-tertiary); - box-shadow: 0 0 12px rgba(233, 69, 96, 0.3); + border-color: var(--accent); + background: var(--bg-panel); + box-shadow: 0 0 0 1px var(--accent-border); } .asset-thumbnail { - width: 100%; - aspect-ratio: 16 / 9; - background-color: var(--bg-tertiary); - display: flex; - align-items: center; - justify-content: center; - font-size: 10px; - color: var(--text-secondary); - overflow: hidden; - flex-shrink: 0; + width: 100%; + aspect-ratio: 16 / 9; + background: var(--bg-deep); + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + color: var(--text-tertiary); + overflow: hidden; + flex-shrink: 0; } .asset-thumbnail img { - width: 100%; - height: 100%; - object-fit: cover; + width: 100%; + height: 100%; + object-fit: cover; } .asset-info { - padding: 8px; - display: flex; - flex-direction: column; - gap: 4px; - flex: 1; - min-height: 0; + padding: var(--sp-2); + display: flex; + flex-direction: column; + gap: 3px; + flex: 1; + min-height: 0; } .asset-filename { - font-weight: 600; - font-size: 11px; - color: var(--text-primary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + font-weight: 500; + font-size: 11px; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .asset-meta { - display: flex; - gap: 4px; - font-size: 10px; - color: var(--text-secondary); + display: flex; + gap: var(--sp-1); + font-size: 10px; + color: var(--text-secondary); + font-family: var(--font-mono); } .asset-status-badge { - display: inline-block; - padding: 2px 6px; - border-radius: 3px; - font-size: 9px; - font-weight: 600; - text-transform: uppercase; - width: fit-content; + display: inline-flex; + align-items: center; + padding: 1px 6px; + border-radius: 100px; + font-size: 9px; + font-weight: 500; + letter-spacing: 0.04em; + text-transform: uppercase; + width: fit-content; } .status-badge.ready { - background-color: rgba(0, 208, 132, 0.2); - color: var(--success); + background: var(--status-green-bg); + color: var(--status-green); } .status-badge.processing { - background-color: rgba(255, 184, 77, 0.2); - color: var(--warning); - animation: pulse 1.5s ease-in-out infinite; + background: var(--status-blue-bg); + color: var(--status-blue); + animation: pulse 1.5s ease-in-out infinite; } .status-badge.error { - background-color: rgba(255, 68, 68, 0.2); - color: var(--error); + background: var(--status-red-bg); + color: var(--status-red); } -/* Empty state */ +/* ================================================================ + EMPTY STATE + ================================================================ */ + .empty-state { - display: flex; - align-items: center; - justify-content: center; - padding: 24px; - text-align: center; - color: var(--text-secondary); - flex-direction: column; - gap: 12px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--sp-12) var(--sp-8); + text-align: center; + gap: var(--sp-2); } .empty-state-icon { - font-size: 32px; - opacity: 0.5; + color: var(--text-tertiary); + margin-bottom: var(--sp-1); } -/* Details panel */ +.empty-state-icon svg { + width: 36px; + height: 36px; +} + +.empty-state-title { + font-size: var(--text-sm); + font-weight: 500; + color: var(--text-secondary); +} + +.empty-state-body { + font-size: var(--text-xs); + color: var(--text-tertiary); + max-width: 30ch; + line-height: 1.5; +} + +/* ================================================================ + DETAILS PANEL + ================================================================ */ + .details-panel { - width: 200px; - background-color: var(--bg-secondary); - border-left: 1px solid var(--border); - padding: 12px; - display: flex; - flex-direction: column; - gap: 12px; - overflow-y: auto; + width: 190px; + background: var(--bg-panel); + border-left: 1px solid var(--border); + padding: var(--sp-3); + display: flex; + flex-direction: column; + gap: var(--sp-2); + overflow-y: auto; } -.details-panel.hidden { - display: none; +.details-panel.hidden { display: none; } + +.details-header { + margin-bottom: var(--sp-1); +} + +.details-header-label { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-tertiary); } .details-section { - display: flex; - flex-direction: column; - gap: 6px; + display: flex; + flex-direction: column; + gap: 2px; } .details-label { - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - color: var(--text-secondary); - letter-spacing: 0.5px; + font-size: 9px; + font-weight: 500; + text-transform: uppercase; + color: var(--text-tertiary); + letter-spacing: 0.06em; } .details-value { - font-size: 12px; - color: var(--text-primary); - word-break: break-word; + font-size: 11px; + color: var(--text-primary); + word-break: break-word; } .tags-list { - display: flex; - flex-wrap: wrap; - gap: 4px; + display: flex; + flex-wrap: wrap; + gap: var(--sp-1); } .tag { - display: inline-block; - background-color: var(--bg-tertiary); - border: 1px solid var(--border); - color: var(--text-secondary); - padding: 4px 8px; - border-radius: 3px; - font-size: 10px; + display: inline-block; + background: var(--bg-surface); + border: 1px solid var(--border-faint); + color: var(--text-secondary); + padding: 2px 6px; + border-radius: 100px; + font-size: 9px; + font-family: var(--font-mono); } .divider { - height: 1px; - background-color: var(--border); + height: 1px; + background: var(--border); + border: none; } -/* Progress indicator */ +/* ================================================================ + PROGRESS INDICATOR + ================================================================ */ + .progress-container { - padding: 12px; - background-color: var(--bg-secondary); - border-bottom: 1px solid var(--border); - display: none; + padding: var(--sp-2) var(--sp-3); + background: var(--bg-panel); + border-bottom: 1px solid var(--border); + display: none; } -.progress-container.visible { - display: block; -} +.progress-container.visible { display: block; } .progress-label { - font-size: 11px; - margin-bottom: 6px; - color: var(--text-secondary); + font-size: var(--text-xs); + margin-bottom: var(--sp-1); + color: var(--text-secondary); + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; } .progress-bar { - width: 100%; - height: 4px; - background-color: var(--bg-tertiary); - border-radius: 2px; - overflow: hidden; + width: 100%; + height: 3px; + background: var(--bg-hover); + border-radius: 2px; + overflow: hidden; } .progress-fill { - height: 100%; - background-color: var(--accent); - width: 0%; - transition: width 0.3s; + height: 100%; + background: var(--accent); + width: 0%; + transition: width 300ms ease-out; } -/* Export timeline panel */ +/* ================================================================ + EXPORT PANEL (Push Timeline to MAM) + ================================================================ */ + .export-panel { - background-color: var(--bg-secondary); - border-top: 2px solid var(--accent); - padding: 12px; - flex-shrink: 0; + background: var(--bg-panel); + border-top: 1px solid var(--border); + padding: var(--sp-3); + flex-shrink: 0; } -.export-panel.hidden { - display: none; -} +.export-panel.hidden { display: none; } .export-panel-title { - font-size: 10px; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.5px; - color: var(--accent); - margin-bottom: 10px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--accent); + margin-bottom: var(--sp-2); } -.export-panel input[type="text"] { - width: 100%; - background-color: var(--bg-tertiary); - border: 1px solid var(--border); - color: var(--text-primary); - padding: 6px 8px; - border-radius: 4px; - font-size: 11px; - margin-bottom: 6px; - transition: border-color 0.2s; +.export-panel input[type="text"], +.export-panel .input { + width: 100%; + height: 28px; + padding: 0 var(--sp-2); + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--r-sm); + color: var(--text-primary); + font-size: var(--text-xs); + margin-bottom: var(--sp-1); + outline: none; + transition: border-color var(--t-fast), box-shadow var(--t-fast); } -.export-panel input[type="text"]:focus { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 2px rgba(233, 69, 96, 0.1); +.export-panel input[type="text"]:focus, +.export-panel .input:focus { + border-color: var(--accent-border); + box-shadow: 0 0 0 2px var(--accent-subtle); } .export-panel select { - width: 100%; - margin-bottom: 8px; - flex: none; + width: 100%; + margin-bottom: var(--sp-2); + flex: none; } .export-clip-info { - font-size: 10px; - color: var(--text-secondary); - margin-bottom: 8px; + font-size: var(--text-xs); + color: var(--text-secondary); + margin-bottom: var(--sp-2); + font-family: var(--font-mono); } .export-panel-actions { - display: flex; - gap: 8px; + display: flex; + gap: var(--sp-2); } -.export-panel-actions button { - flex: 1; +.export-panel-actions .btn { flex: 1; } + +/* ================================================================ + ADVANCED SECTION + ================================================================ */ + +.advanced-section { + background: var(--bg-panel); + border-bottom: 1px solid var(--border); + padding: var(--sp-2) var(--sp-3); + flex-shrink: 0; } -/* Action bar — two rows */ +.advanced-section-title { + font-size: 9px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--text-tertiary); + margin-bottom: var(--sp-1); +} + +.advanced-row { + display: flex; + gap: var(--sp-2); +} + +.advanced-row .btn { flex: 1; } + +/* ================================================================ + ACTION BAR + ================================================================ */ + .action-bar { - background-color: var(--bg-secondary); - border-top: 1px solid var(--border); - padding: 10px 12px; - flex-shrink: 0; - display: flex; - flex-direction: column; - gap: 6px; + background: var(--bg-panel); + border-top: 1px solid var(--border); + padding: var(--sp-2) var(--sp-3); + flex-shrink: 0; + display: flex; + flex-direction: column; + gap: var(--sp-1); } .action-row { - display: flex; - gap: 8px; + display: flex; + gap: var(--sp-2); } -.action-row button { - flex: 1; +.action-row .btn { flex: 1; } + +/* ================================================================ + SLIDE PANEL (design system pattern) + ================================================================ */ + +.slide-overlay { + position: fixed; + inset: 0; + background: oklch(8% 0.010 250 / 0.65); + z-index: 80; + opacity: 0; + pointer-events: none; + transition: opacity var(--t-normal); } -/* Loading spinner */ -.spinner { - display: inline-block; - width: 12px; - height: 12px; - border: 2px solid var(--border); - border-top-color: var(--accent); - border-radius: 50%; - animation: spin 0.8s linear infinite; +.slide-overlay.open { opacity: 1; pointer-events: all; } + +.slide-panel { + position: fixed; + top: 0; right: 0; bottom: 0; + width: 420px; + background: var(--bg-panel); + border-left: 1px solid var(--border); + z-index: 90; + display: flex; + flex-direction: column; + transform: translateX(100%); + transition: transform var(--t-normal); } -@keyframes spin { - to { transform: rotate(360deg); } +.slide-panel.open { transform: translateX(0); } + +.slide-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 var(--sp-5); + height: 40px; + border-bottom: 1px solid var(--border); + flex-shrink: 0; } -/* Responsive layout for smaller panels */ -@media (max-width: 500px) { - .asset-grid { - grid-template-columns: repeat(auto-fill, minmax(95px, 1fr)); - gap: 10px; - padding: 10px; - } - - .details-panel { - width: 150px; - padding: 10px; - } - - .asset-card { - height: 150px; - } - - .connection-controls { - grid-template-columns: 1fr; - } - - .server-input-group { - grid-column: 1; - width: 100%; - } - - button.connect-btn { - grid-column: 2 / 4; - } +.slide-panel-title { + font-size: var(--text-sm); + font-weight: 500; + color: var(--text-primary); } -/* Utility classes */ -.hidden { - display: none !important; +.slide-panel-body { + flex: 1; + overflow-y: auto; + padding: var(--sp-5); + display: flex; + flex-direction: column; + gap: var(--sp-4); } -.loading { - opacity: 0.6; - pointer-events: none; +.slide-panel-footer { + padding: var(--sp-3) var(--sp-5); + border-top: 1px solid var(--border); + display: flex; + justify-content: flex-end; + gap: var(--sp-2); + flex-shrink: 0; } +/* ================================================================ + PRESET CARDS (for FCP Conform) + ================================================================ */ + +.preset-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--sp-2); +} + +.preset-card { + padding: var(--sp-2) var(--sp-2); + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--r-md); + cursor: pointer; + transition: border-color var(--t-fast), background var(--t-fast); +} + +.preset-card:hover { + border-color: var(--accent-border); + background: var(--bg-raised); +} + +.preset-card.selected { + border-color: var(--accent); + background: var(--accent-subtle); +} + +.preset-card-title { + font-size: var(--text-xs); + font-weight: 500; + color: var(--text-primary); + margin-bottom: 2px; +} + +.preset-card-desc { + font-size: 10px; + color: var(--text-tertiary); + font-family: var(--font-mono); +} + +/* ================================================================ + CLIP LIST (for Hi-Res Relink) + ================================================================ */ + +.clip-list-container { + flex: 1; + min-height: 0; + overflow-y: auto; + border: 1px solid var(--border); + border-radius: var(--r-md); + background: var(--bg-base); +} + +.clip-list { + display: flex; + flex-direction: column; +} + +.clip-list-item { + display: flex; + align-items: center; + gap: var(--sp-2); + padding: var(--sp-2) var(--sp-2); + border-bottom: 1px solid var(--border-faint); + transition: background var(--t-fast); +} + +.clip-list-item:last-child { border-bottom: none; } + +.clip-list-item:hover { background: var(--bg-surface); } + +.clip-list-item-checkbox { + width: 14px; + height: 14px; + accent-color: var(--accent); + flex-shrink: 0; + cursor: pointer; +} + +.clip-list-item-info { + flex: 1; + min-width: 0; +} + +.clip-list-item-name { + font-size: var(--text-xs); + font-weight: 500; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.clip-list-item-meta { + font-size: 10px; + color: var(--text-tertiary); + font-family: var(--font-mono); +} + +.clip-list-item-status { + flex-shrink: 0; + font-size: 9px; + font-weight: 500; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.clip-list-item-status.matched { + color: var(--status-green); +} + +.clip-list-item-status.unmatched { + color: var(--text-disabled); +} + +/* ================================================================ + RELINK SUMMARY + ================================================================ */ + +.relink-summary { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--sp-2); + padding: var(--sp-4); + text-align: center; +} + +.relink-summary.hidden { display: none; } + +.relink-summary-icon { color: var(--status-green); } + +.relink-summary-text { + font-size: var(--text-sm); + font-weight: 500; + color: var(--text-primary); +} + +.relink-summary-detail { + font-size: var(--text-xs); + color: var(--text-secondary); + font-family: var(--font-mono); +} + +/* ================================================================ + MESSAGE BANNERS + ================================================================ */ + .error-message { - background-color: rgba(255, 68, 68, 0.1); - border: 1px solid var(--error); - color: var(--error); - padding: 8px 12px; - border-radius: 4px; - font-size: 11px; - margin-bottom: 8px; + background: var(--status-red-bg); + border: 1px solid oklch(64% 0.22 25 / 0.25); + color: var(--status-red); + padding: var(--sp-2) var(--sp-3); + border-radius: var(--r-sm); + font-size: var(--text-xs); + margin-bottom: var(--sp-2); } .success-message { - background-color: rgba(0, 208, 132, 0.1); - border: 1px solid var(--success); - color: var(--success); - padding: 8px 12px; - border-radius: 4px; - font-size: 11px; - margin-bottom: 8px; + background: var(--status-green-bg); + border: 1px solid oklch(70% 0.18 148 / 0.25); + color: var(--status-green); + padding: var(--sp-2) var(--sp-3); + border-radius: var(--r-sm); + font-size: var(--text-xs); + margin-bottom: var(--sp-2); } .info-message { - background-color: rgba(233, 69, 96, 0.1); - border: 1px solid var(--accent); - color: var(--accent); - padding: 8px 12px; - border-radius: 4px; - font-size: 11px; - margin-bottom: 8px; + background: var(--accent-subtle); + border: 1px solid var(--accent-border); + color: var(--accent); + padding: var(--sp-2) var(--sp-3); + border-radius: var(--r-sm); + font-size: var(--text-xs); + margin-bottom: var(--sp-2); } -/* Text truncation */ +/* ================================================================ + FORM GROUP (design system) + ================================================================ */ + +.form-group { + display: flex; + flex-direction: column; + gap: var(--sp-1); +} + +.form-label { + font-size: 10px; + font-weight: 500; + letter-spacing: 0.07em; + text-transform: uppercase; + color: var(--text-secondary); +} + +.form-hint { + font-size: var(--text-xs); + color: var(--text-tertiary); + line-height: 1.5; +} + +/* ================================================================ + UTILITY + ================================================================ */ + +.hidden { display: none !important; } + +.loading { opacity: 0.6; pointer-events: none; } + .truncate { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .truncate-lines-2 { - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.spinner { + display: inline-block; + width: 12px; + height: 12px; + border: 2px solid var(--border); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* ================================================================ + RESPONSIVE + ================================================================ */ + +@media (max-width: 500px) { + .asset-grid { + grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); + gap: var(--sp-2); + padding: var(--sp-2); + } + + .details-panel { + width: 140px; + padding: var(--sp-2); + } + + .asset-card { height: 145px; } + + .slide-panel { width: 100%; } } diff --git a/services/premiere-plugin/docs/ADVANCED_FEATURES.md b/services/premiere-plugin/docs/ADVANCED_FEATURES.md new file mode 100644 index 0000000..3a4c73f --- /dev/null +++ b/services/premiere-plugin/docs/ADVANCED_FEATURES.md @@ -0,0 +1,177 @@ +# Premiere Plugin: Advanced Features Guide + +## Overview + +The Wild Dragon MAM Premiere Pro CEP Panel includes two advanced production features: **FCP XML Export & Conform** and **Hi-Res Auto-Relink**. These features streamline the post-production workflow by enabling timeline-level round-tripping between proxy editing and conformed delivery. + +--- + +## 1. FCP XML Export & Conform + +Export your Premiere Pro timeline as a Final Cut Pro XML, conform it server-side via FFmpeg, and return it as a new MAM asset. + +### Use Cases + +- **Proxy-to-Hi-Res Conform**: Edit with low-res proxies, then produce a full-resolution master +- **Broadcast Delivery**: Export with broadcast-quality codec presets (ProRes, DNxHR-equivalent H.264) +- **Multi-Format Output**: Generate web, archive, and broadcast versions from a single timeline +- **Remote Conform**: Upload the timeline XML to the server; the server handles the heavy FFmpeg rendering + +### Step-by-Step Workflow + +1. Ensure you are connected to a MAM server and have an active sequence in Premiere Pro +2. Click **Advanced > Export & Conform Timeline** +3. In the slide panel that opens, configure export settings: + - **Preset**: Choose one of the four presets (Broadcast, Web, Archive, Custom) + - **Codec**: H.264, H.265/HEVC, or ProRes + - **Quality**: Low, High, or Broadcast-grade + - **Resolution**: Match sequence, 4K, 1080p, or 720p + - **Audio**: Include or exclude +4. Click **Start Conform** +5. The plugin reads the timeline, generates FCP XML, and uploads it to the server +6. A progress bar shows the conform job status (parsing, processing, encoding, uploading) +7. When complete, the conformed asset appears in the MAM asset grid with a name like "Conformed: [Sequence Name]" +8. The new asset can be imported like any other MAM asset + +### Preset Descriptions + +| Preset | Codec | Quality | Resolution | Audio | Use Case | +|--------|-------|---------|------------|-------|----------| +| **Broadcast Quality** | ProRes | Broadcast | Match | Stereo | TV/Cinema delivery | +| **Web Delivery** | H.264 | High | 1080p | AAC | YouTube/Vimeo | +| **Archive** | H.265 | Broadcast | 4K | AAC | Long-term storage | +| **Custom** | User-set | User-set | User-set | Optional | Flexible output | + +### Troubleshooting + +| Issue | Likely Cause | Solution | +|-------|-------------|----------| +| "No active sequence" | No timeline is open in Premiere | Open a sequence and retry | +| Conform job stuck at 0% | Server worker not running | Check that the worker service is running (`services/worker`) | +| "Asset not found for reel" | Source media not in MAM | Import the hi-res originals into MAM first | +| Conform fails with ffmpeg error | Incompatible codec/format | Try a different codec preset | + +### Performance Considerations + +- Conform speed depends on timeline length and server GPU/CPU resources +- A 30-minute timeline typically completes in 5-15 minutes with hardware encoding +- Conform jobs run sequentially (concurrency: 1) to avoid overloading the server +- Large timelines may trigger the 50MB request body limit; if needed, increase `express.json({ limit })` in the API + +--- + +## 2. Hi-Res Auto-Relink + +Detect proxy clip usage in your timeline, render frame-accurate trimmed hi-res segments on the server, and automatically relink every clip. + +### Use Cases + +- **Proxy-to-Hi-Res Upgrade**: Replace every proxy clip in a sequence with the matching hi-res original +- **Multi-Clip Batch Relink**: Relink 5, 10, or 50+ clips in a single operation +- **Frame-Accurate Segments**: Each clip is trimmed to exactly its timeline in/out points (no handles) +- **Selective Relink**: Deselect specific clips you want to keep as proxies + +### Step-by-Step Workflow + +1. Ensure you are connected to a MAM server and have an active sequence in Premiere Pro +2. Click **Advanced > Fetch & Relink All** +3. The plugin analyzes the timeline and shows a clip list with checkboxes: + + ``` + [x] clip_001.mov Track V1 00:00:00:00 - 00:00:05:12 + [x] clip_002.mov Track V1 00:00:05:13 - 00:00:12:06 + [x] clip_003.mov Track V2 00:00:00:00 - 00:00:08:00 + ``` + +4. Review the clip list; uncheck any clips you do not want relinked +5. Click **Fetch & Relink** +6. A confirmation dialog shows the summary (X clips will be relinked) +7. The server trims each hi-res clip to the exact frame range used on the timeline +8. Each trimmed segment is downloaded and automatically relinked in Premiere +9. A completion summary shows: + - Success count (relinked successfully) + - Retry count (relinked after one retry) + - Failed count (could not relink, with error detail) + +### Clip Selection & Filtering + +- Each clip instance (including multiple uses of the same source with different in/out points) is listed separately +- Clips are identified by their timeline track and position +- The plugin resolves each clip's file path to a MAM asset ID using the import mapping + +### Understanding Temp Segments & TTL + +- Each trimmed segment is stored in S3 under `temp-segments/{clipInstanceId}.mov` +- Segments have a 24-hour TTL (time-to-live) +- After 24 hours, expired segments are automatically cleaned up by the server's cleanup task +- The cleanup runs hourly and removes both the S3 objects and the database records +- If a relink job is interrupted, expired segments can be regenerated by running the workflow again + +### Troubleshooting Relink Failures + +| Issue | Likely Cause | Solution | +|-------|-------------|----------| +| "Asset not found" | Source clip not imported through MAM | Import clips into MAM before editing | +| Segment download fails | Network issue or expired S3 URL | Retry the relink | +| "No matching clip" | Timeline clip position changed | Refresh the clip list and retry | +| Relink partially succeeds | Some clips may be locked | Unlock clips in Premiere and retry failed items | + +### Best Practices for Proxy Workflows + +1. **Import through MAM first**: Always import proxy or hi-res files through the plugin to create the file-path-to-asset mapping +2. **Use descriptive filenames**: The plugin matches clips by their file path; unique, stable paths improve reliability +3. **Avoid moving media mid-project**: If source files move, the clip list may show "unmatched" clips +4. **Run after final picture lock**: For best results, relink after editing is complete +5. **Server must have hi-res originals**: The trim worker fetches original files from S3; ensure they are uploaded before relinking + +--- + +## 3. Technical Details + +### Job Queue & FIFO Processing + +- All conform and trim jobs use BullMQ (backed by Redis) +- Conform: FIFO queue, concurrency 1 (one job at a time to avoid resource contention) +- Trim: FIFO queue, concurrency 4 (up to four segments can be rendered simultaneously) +- Job status is tracked in real-time via Server-Sent Events (SSE) from `GET /api/v1/jobs/events` + +### Temp Segment Storage & Cleanup + +- Database table: `temp_segments` +- S3 prefix: `temp-segments/` +- Default TTL: 24 hours (configurable in database) +- Cleanup interval: 3600 seconds (1 hour) +- S3 lifecycle policy: 1-day expiration on `temp-segments/` prefix (backup to database cleanup) + +### Network Requirements + +- The plugin communicates with the MAM API over HTTP/HTTPS +- Conform XML upload may be large (up to 50MB); ensure adequate bandwidth +- Trim segments are downloaded from S3 presigned URLs; the server generates these on demand +- For on-premise deployments, latency between the editor workstation and the server should be under 50ms for responsive polling + +### Performance Impact + +- Timeline reading is fast (< 1 second for 100+ clips) +- Conform jobs use server-side GPU encoding if available (NVIDIA NVENC) +- Trim jobs use stream copy where possible (no re-encode) for speed +- Proxy playback is unaffected during relink operations +- The plugin's UI remains responsive during background operations + +--- + +## Files + +| File | Purpose | +|------|---------| +| `services/premiere-plugin/jsx/premiere.jsx` | ExtendScript: timeline reading, FCP XML export, relink | +| `services/premiere-plugin/js/main.js` | Panel JS: UI, API calls, workflow orchestration | +| `services/premiere-plugin/index.html` | Panel UI: layout, advanced section, slide panels | +| `services/premiere-plugin/css/styles.css` | Panel styles: Wild Dragon design system | +| `services/mam-api/src/routes/assets.js` | API: batch-trim, trim-status endpoints | +| `services/mam-api/src/routes/sequences.js` | API: conform endpoint | +| `services/mam-api/src/routes/jobs.js` | API: job listing, trim queue registration | +| `services/mam-api/src/tasks/cleanupTempSegments.js` | Server: temp segment cleanup task | +| `services/worker/src/workers/conform.js` | Worker: FCP XML parsing, FFmpeg conform | +| `services/worker/src/workers/trimWorker.js` | Worker: frame-accurate FFmpeg trimming | +| `services/worker/src/index.js` | Worker: queue registration | diff --git a/services/premiere-plugin/docs/TESTING_ADVANCED_FEATURES.md b/services/premiere-plugin/docs/TESTING_ADVANCED_FEATURES.md new file mode 100644 index 0000000..f4d967d --- /dev/null +++ b/services/premiere-plugin/docs/TESTING_ADVANCED_FEATURES.md @@ -0,0 +1,374 @@ +# End-to-End Testing: Advanced Features + +## Overview + +This document covers the comprehensive testing plan for the FCP XML Export & Conform and Hi-Res Auto-Relink features. + +## Prerequisites + +- Running MAM API server (`services/mam-api`) +- Running Worker service (`services/worker`) +- Redis instance (for BullMQ) +- S3-compatible storage +- Premiere Pro 2024 (Windows 10/11 or macOS 13+) +- Premiere Pro CEP panel installed and connected to the MAM server +- At least one project with assets in the MAM +- Assets should have hi-res originals uploaded to S3 + +--- + +## FCP XML Export & Conform + +### Test 1: Simple Timeline Export + +1. Create a new sequence in Premiere with a single video track and 3 clips +2. Each clip should be imported through the MAM plugin (to create the asset mapping) +3. Click **Advanced > Export & Conform Timeline** +4. Select the "Web Delivery" preset +5. Click **Start Conform** +6. **Expected**: Slide panel closes, progress bar shows job status +7. Monitor the job via the Jobs page in the web UI +8. **Expected**: Job transitions through parsing, processing, encoding, uploading +9. **Expected**: New asset appears in the grid with name "Conformed: [Sequence Name]" +10. Import the conformed asset and verify playback + +### Test 2: Complex Timeline (Multi-Track, Transitions) + +1. Create a sequence with 3 video tracks and 6+ clips total +2. Include video transitions (cross-dissolve, dip to color) +3. Some clips should have speed/duration changes +4. Export and conform using "Broadcast Quality" preset (ProRes) +5. **Expected**: All clips are processed; transitions are rendered into the output +6. **Expected**: Output codec matches ProRes, high quality + +### Test 3: All Codec Presets + +| Preset | Codec | Expected Extension | +|--------|-------|--------------------| +| Broadcast Quality | ProRes | .mov | +| Web Delivery | H.264 | .mp4 | +| Archive | H.265/HEVC | .mp4 | +| Custom (H.264) | H.264 | .mp4 | +| Custom (ProRes) | ProRes | .mov | + +1. Run conform job for each preset +2. Verify the output codec via `ffprobe` or Premiere's project panel + +### Test 4: All Resolution Options + +1. Test with "Match Sequence" - output should match the sequence resolution +2. Test "4K" - output should be 3840x2160 +3. Test "1080p" - output should be 1920x1080 +4. Test "720p" - output should be 1280x720 + +### Test 5: Frame Rate Handling + +Test with sequences at each frame rate: + +| Frame Rate | Type | Notes | +|------------|------|-------| +| 23.976 fps | Non-drop | Common for cinema | +| 24 fps | Non-drop | Film standard | +| 25 fps | Non-drop | PAL standard | +| 29.97 fps | Drop-frame | NTSC standard | +| 30 fps | Non-drop | Progressive | +| 59.94 fps | Drop-frame | High-frame rate NTSC | +| 60 fps | Non-drop | High-frame rate progressive | + +Verify: +- Timecodes are correct in output +- No frame rate mismatch warnings +- Audio sync is maintained + +### Test 6: Job Cancellation + +1. Start a conform job with a long timeline (5+ minutes) +2. Before it completes, navigate to the Jobs page +3. Cancel the job +4. **Expected**: Job status changes to "cancelled" or "failed" +5. **Expected**: No orphaned partial output in S3 + +### Test 7: Network Failure During Upload + +1. Start a conform job +2. Disconnect the network while the FCP XML is uploading +3. **Expected**: Error message displayed in the plugin +4. Reconnect and retry the job +5. **Expected**: Job completes on retry + +### Test 8: Server Failure During Conform + +1. Start a conform job +2. Restart the worker service while the job is active +3. **Expected**: Job is stalled then moved back to "waiting" by BullMQ's stall detection +4. **Expected**: Worker picks it up on restart and completes it + +### Test 9: Verify Conformed Asset in MAM + +1. After conform completes, locate the new asset in the MAM grid +2. Verify metadata: correct codec, resolution, frame rate, duration +3. Verify the conformed asset has the `conform_source_sequence_id` set +4. Import the asset into a new Premiere sequence +5. Verify playback and audio sync + +### Test 10: Re-Importing Conformed Asset + +1. After conform, import the conformed asset into a new Premiere project +2. **Expected**: Asset imports and plays correctly +3. Verify the asset shows up in the imported assets list + +--- + +## Hi-Res Auto-Relink + +### Test 11: Relink Single Clip + +1. Have a sequence with 1 clip that was imported via MAM proxy workflow +2. Click **Advanced > Fetch & Relink All** +3. **Expected**: Clip list shows 1 clip with checkbox checked +4. Click **Fetch & Relink** +5. **Expected**: Server trims the hi-res original to match the timeline clip +6. **Expected**: Clip is relinked in Premiere +7. **Expected**: Success summary shows 1 clip relinked + +### Test 12: Relink Multiple Clips (5-10) + +1. Create a sequence with 5-10 clips from various assets +2. Run the relink workflow +3. **Expected**: All clips are processed in parallel (concurrency up to 4) +4. **Expected**: All clips are relinked successfully + +### Test 13: Same Asset, Multiple Instances + +1. Create a sequence that uses the same source asset in 3 different positions +2. Each instance should have different in/out points (different trim ranges) +3. Run the relink workflow +4. **Expected**: Each instance gets a separate trimmed segment (different clipInstanceId) +5. **Expected**: Each instance is individually relinked to its trimmed segment + +### Test 14: Various Codecs + +Test with source clips in each codec: + +| Codec | Format | Expected | +|-------|--------|----------| +| ProRes 422 | .mov | Segment trims correctly | +| ProRes 4444 | .mov | Alpha channel preserved (if applicable) | +| DNxHR HQ | .mxf | Segment trims correctly | +| H.264 | .mp4 | Segment trims correctly | +| HEVC/H.265 | .mp4 | Segment trims correctly | +| XDCAM | .mxf | Segment trims correctly | + +### Test 15: Selective Relink (Deselect Some Clips) + +1. Create a sequence with 5 clips +2. Open the clip list and uncheck 2 clips +3. Click **Fetch & Relink** +4. **Expected**: Only the 3 checked clips are relinked +5. **Expected**: The 2 unchecked clips remain as proxies + +### Test 16: Retry Logic for Failed Relinks + +1. Simulate a network error during segment download (disconnect Wi-Fi) +2. Start the relink workflow +3. **Expected**: Failed clip shows "retry" in the summary +4. Reconnect and retry +5. **Expected**: Failed clips are retried and succeed on second attempt + +### Test 17: Locked Premiere Project + +1. With a collaborator's project open (if available), or a read-only project +2. Run the relink workflow +3. **Expected**: Appropriate error message about project being locked +4. **Expected**: Plugin does not crash + +### Test 18: Missing Hi-Res Originals + +1. Create a sequence with a clip whose hi-res original has been deleted from S3 +2. Run the relink workflow +3. **Expected**: The worker reports "Asset not found" for that clip +4. **Expected**: Summary shows that clip as failed with appropriate error + +### Test 19: Network Failure During Download + +1. Start the relink workflow with 3+ clips +2. Disconnect the network during the download phase +3. **Expected**: Download fails with network error +4. **Expected**: Summary shows failed clips with "download error" message +5. Reconnect and retry +6. **Expected**: Succeeds on retry + +### Test 20: Temp Segment Cleanup After 24 Hours + +1. Complete a relink workflow (creates temp segments) +2. Verify the temp segment records exist in the database +3. Fast-forward the system clock by 25 hours (or set `expires_at` to the past) +4. Wait for the cleanup task to run (within 1 hour) +5. **Expected**: Temp segments are deleted from S3 and database +6. **Expected**: Cleanup log shows "X deleted, 0 errors" + +--- + +## GUI Redesign + +### Test 21: Visual Consistency with Web-UI + +1. Open the Premiere plugin panel +2. Compare colors with the web-ui (open in browser) +3. Verify OKLCH color tokens match: + - Background surfaces (bg-panel, bg-surface, bg-raised) + - Text hierarchy (primary, secondary, tertiary) + - Accent color is the same blue-purple hue + - Status colors (green, red, yellow) match +4. **Expected**: Consistent visual identity between web and Premiere panel + +### Test 22: Responsive Layout + +1. Dock the panel at various widths (200px, 400px, 600px) +2. Verify the layout adjusts: + - Asset grid columns adapt to available width + - Buttons remain visible at all widths + - Details panel is usable at all widths + - Slide panels render correctly + +### Test 23: Color Contrast (WCAG AA) + +1. Measure contrast ratios: + - Text primary on bg-panel: should be ≥ 4.5:1 + - Text secondary on bg-panel: should be ≥ 3:1 + - Accent text on accent background: should be ≥ 3:1 +2. **Expected**: All text meets WCAG AA contrast requirements + +### Test 24: Font Rendering + +1. Verify body text renders in Inter (or system fallback) +2. Verify metadata and technical values render in JetBrains Mono +3. Check font loading from web-ui server (not bundled): + - Look for 404s in the console + - Verify fonts load after server connection is established + +### Test 25: Button States + +Test each button for all states: + +| State | Visual Indicator | +|-------|------------------| +| Default | Normal styling | +| Hover | Background lightens, subtle lift | +| Active/Pressed | Returns to default position | +| Disabled | 38% opacity, no pointer events | + +### Test 26: Slide Panel Animations + +1. Open the Export & Conform slide panel +2. **Expected**: Panel slides in from the right (translateX animation) +3. Close the panel +4. **Expected**: Panel slides out to the right +5. Test the same with the Relink clip list panel +6. **Expected**: Smooth 250ms ease-out transitions + +### Test 27: Progress Indicators + +1. Start any long-running operation (conform, relink) +2. **Expected**: Progress bar appears with percentage +3. **Expected**: Progress bar fills smoothly +4. **Expected**: Status label updates with current operation +5. **Expected**: Progress bar disappears on completion + +### Test 28: Toast Notifications + +1. Perform actions that trigger messages: + - Connect to server → "Connected..." (success) + - Failed connection → "Failed to connect" (error) + - Import complete → "Imported" (success) + - Relink summary → shows success/failure counts (info) +2. **Expected**: Messages appear at the top of the panel +3. **Expected**: Messages auto-dismiss after 5 seconds +4. **Expected**: Success = green, Error = red, Info = accent blue + +--- + +## Performance Testing + +### Test 29: Conform Job with 30-Minute Timeline + +1. Create a sequence with 30+ minutes of clips +2. Run the conform workflow with H.264 Web Delivery preset +3. Measure: + - XML generation time (< 2 seconds) + - Upload time (depends on network) + - Server parsing time (< 5 seconds) + - FFmpeg transcode time (varies by server GPU/CPU) + - Total time +4. **Expected**: Completes within reasonable time (under 30 minutes on GPU-enabled server) + +### Test 30: Relink 50+ Clips Simultaneously + +1. Create a sequence with 50+ clips (can duplicate assets) +2. Run the relink workflow +3. Measure: + - Timeline analysis time + - Server trim queue time + - Total download time +4. **Expected**: All clips processed within reasonable time +5. **Expected**: No timeout errors + +### Test 31: Multiple Concurrent Conform Jobs + +1. Start 3 conform jobs in rapid succession +2. **Expected**: Jobs are queued FIFO +3. **Expected**: Only 1 job is active at a time (concurrency: 1) +4. **Expected**: Queued jobs show "waiting" status +5. **Expected**: Each job completes in turn without data corruption + +### Test 32: Server Load During Peak Usage + +1. While a conform job is running, perform other operations: + - Browse assets in the web UI + - Upload new files + - Search the asset catalog +2. **Expected**: API remains responsive (under 500ms response time) +3. **Expected**: Conform job continues without error + +### Test 33: Premiere Pro Responsiveness + +1. Start a relink workflow for 10 clips +2. During the operation, interact with Premiere: + - Scrub the timeline + - Open other panels + - Play back the sequence +3. **Expected**: Premiere remains responsive (no freezing) + +--- + +## Test Environments + +| Environment | OS | Premiere Version | Notes | +|-------------|----|------------------|-------| +| **Workstation A** | Windows 11 | Premiere Pro 2024 (v24.x) | Primary test environment | +| **Workstation B** | Windows 10 | Premiere Pro 2024 (v24.x) | Legacy Windows | +| **Workstation C** | macOS 14 Sonoma | Premiere Pro 2024 (v24.x) | macOS testing | +| **Workstation D** | macOS 13 Ventura | Premiere Pro 2024 (v24.x) | Legacy macOS | + +### Network Profiles + +| Profile | Bandwidth | Latency | Notes | +|---------|-----------|---------|-------| +| Local | 1 Gbps | < 1ms | Server on same LAN | +| Fast WAN | 100 Mbps | 10ms | Remote server | +| Slow WAN | 10 Mbps | 50ms | VPN or limited connection | + +--- + +## Acceptance Criteria + +- [ ] All test scenarios pass +- [ ] No crashes or data loss in any tested configuration +- [ ] Clear error messages displayed for all failure modes +- [ ] Performance meets expectations across all tested environments +- [ ] UI matches the Wild Dragon design system specification +- [ ] Font loading from web-ui server works (no bundled fonts) +- [ ] Temp segment cleanup runs and removes expired data +- [ ] Slide panel animations are smooth and meet motion specs +- [ ] All button states render correctly +- [ ] Color contrast meets WCAG AA standards diff --git a/services/premiere-plugin/index.html b/services/premiere-plugin/index.html index 85d8709..7f5583f 100644 --- a/services/premiere-plugin/index.html +++ b/services/premiere-plugin/index.html @@ -27,10 +27,10 @@ id="api-token" class="server-url" placeholder="API token (wd_…)" - title="API token — create with POST /api/v1/tokens" + title="API token" autocomplete="off" > - + @@ -52,11 +52,11 @@ - + @@ -65,14 +65,24 @@
-
📁
-
Connect to server and load assets
+
+ + + + +
+
No assets
+
Connect to server and load assets
- + + + +
+
Advanced
+
+ +
-
- - + +
-
- - + +
-
- - + +
+ +
+
+
+ Export & Conform + +
+
+ +
+ Preset +
+
+
Broadcast
+
ProRes 422 HQ, 1920x1080, 48kHz
+
+
+
Web
+
H.264, 1920x1080, AAC 320kbps
+
+
+
Archive
+
ProRes 4444, UHD, 48kHz
+
+
+
Custom
+
Manual settings
+
+
+
+ + +
+ Codec + +
+ + +
+ Quality + +
+ + +
+ Resolution + +
+ + +
+ Audio Preset + +
+ + +
+ Timeline Clips +
+
+
+ +
+ + + + + - diff --git a/services/premiere-plugin/js/main.js b/services/premiere-plugin/js/main.js index 536e340..5ac9638 100644 --- a/services/premiere-plugin/js/main.js +++ b/services/premiere-plugin/js/main.js @@ -1,13 +1,9 @@ /** * Wild Dragon MAM - Premiere Pro Panel * Main JavaScript file for the CEP panel + * Features: #30 FCP XML Export & Conform, #31 Hi-Res Auto-Relink, #32 GUI Redesign */ -// Adobe CEP interface — CSInterface.js already declared `const csInterface` -// at the script-realm scope (top-level `const` is shared across non-module -//