XML Export for Adobe Premiere Pro — Investigation Report
Date: 2026-02-09 Status: Active investigation Goal: XMEML export with correct captions for Adobe Premiere Pro video editors
Table of Contents
- What the Export Provides
- Architecture & Data Flow
- Data Sources — Where the Data Comes From
- What the Code Does to the Data
- Current State of the Effort
- Recent Changes
- Risk Assessment — Will It Still Work?
- Identified Issues
- Recommendations for Correct Captions in Premiere
- File Reference
1. What the Export Provides
The export produces a ZIP file containing:
VideoProject_[timestamp].zip
├── videos/
│ ├── video_1.mp4
│ ├── video_2.mp4
│ └── ...
├── project_[timestamp].xml ← Adobe Premiere Pro XMEML v4 timeline
├── combined_srt_file.srt ← Subtitles for the full timeline
└── README.txt ← Instructions for the editor
What each file does:
| File | Format | Purpose |
|---|---|---|
project_*.xml | XMEML v4 | Premiere Pro timeline with video+audio tracks, silence/filler cuts applied |
combined_srt_file.srt | SubRip (SRT) | Captions for the entire timeline, timing-adjusted for silence removal |
videos/*.mp4 | MP4 | Raw source video files downloaded from cloud storage |
README.txt | Text | Setup instructions for the video editor |
Key characteristics:
- Format: XMEML v4 (Adobe Premiere Pro legacy XML interchange, NOT Final Cut Pro XML)
- Resolution: 1920x1080 (or source resolution when available)
- Frame rate: Source frame rate or 30fps default, NTSC mode
- Audio: 2-channel stereo, 16-bit, 44.1kHz
- Codec metadata: Apple ProRes 422 (metadata only — source files are MP4)
- Silence removal: Automatic — silence and filler ranges are cut from the timeline
- Captions: Separate SRT file (NOT embedded in the XML timeline)
2. Architecture & Data Flow
High-Level Pipeline
┌─────────────────────────────────────────────────────────────────┐
│ ADMIN APP: /video-exporter?videoFlowId=X&respondentId=Y │
│ (apps/admin/app/video-exporter/page.tsx) │
└──────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ SERVER: Fetch respondent data + company context │
│ • getVideoProcessingRegistry().getRespondentData(flowId, id) │
│ • getVideoFlowCompanyContext(videoFlowId) │
└──────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ CLIENT: VideoProjectComposer (React component) │
│ (apps/admin/components/features/video-editing/ │
│ VideoProjectComposer.tsx) │
│ │
│ User clicks "Download Complete Project Package" │
└──────────────────────────┬──────────────────────────────────────┘
│
┌────────────┴────────────┐
▼ ▼
┌──────────────────────┐ ┌──────────────────────────────────────┐
│ BROWSER: │ │ Step 2: POST /api/export-xml │
│ Step 1: Download │ │ (apps/admin/app/api/export-xml/ │
│ videos in parallel │ │ route.ts) │
│ via fetch() │ │ │
│ (useVideoDownloader)│ │ Delegates to VideoExportService │
└──────────────────────┘ └──────────────┬───────────────────────┘
│
┌──────────────────────────┴──────────────────┐
▼ ▼
┌───────────────────────────┐ ┌────────────────────────────┐
│ XML Generation │ │ SRT Generation │
│ (xml-export.ts) │ │ (video-export-service.ts │
│ │ │ + timing-coordinator.ts │
│ • Build VideoProcessing │ │ + srt.ts) │
│ Results from registry │ │ │
│ • Split clips at silence │ │ • Use corrected or raw │
│ /filler boundaries │ │ transcription │
│ • Generate XMEML v4 │ │ • Adjust timing for │
│ with video+audio │ │ silence removal │
│ tracks │ │ • Format as SRT │
└───────────┬───────────────┘ └────────────┬───────────────┘
│ │
└───────────────┬───────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ BROWSER: Create ZIP (JSZip) │
│ • Add video blobs to videos/ folder │
│ • Add XML content │
│ • Add SRT content │
│ • Add README │
│ • Trigger browser download │
└─────────────────────────────────────────────────────────────────┘
Detailed Data Flow per Step
Step 1: Page Load (Server)
URL: /video-exporter?videoFlowId=abc&respondentId=xyz&source=admin
│
▼
┌───────────────────────────────────┐
│ VideoProcessingRegistry (Redis) │
│ .getRespondentData(flowId, id) │
│ │
│ Returns: Session { │
│ sessionId, │
│ userData: { name, email }, │
│ responses: QuestionResponse[] │
│ } │
└───────────────────────────────────┘
Step 2: Export Trigger (Client → Server → Client)
handleExport() in VideoProjectComposer
│
├─ 1. downloadVideosToLocal(videoUrls)
│ • Parallel fetch of all video URLs
│ • Returns DownloadedFile[] with blobs + localPaths
│
├─ 2. POST /api/export-xml
│ • Sends: downloadedFiles, videoFlowId, respondentId
│ • Server fetches session data again from registry
│ • Maps QuestionResponse[] → VideoProcessingResult[]
│ • Generates XML via generateXmlString()
│ • Generates SRT via generateCombinedSrtFile()
│ • Returns: { xmlContent, videoProcessingResults, combinedSrtFile }
│
└─ 3. createAndDownloadZip(files, xml, srt)
• Packages everything into ZIP
• Triggers browser download
3. Data Sources — Where the Data Comes From
Primary Data Source: VideoProcessingRegistry (Redis KV)
All video data is stored in Redis via @repo/registries and accessed through VideoProcessingRegistry.
VideoProcessingRegistry.getRespondentData(videoFlowId, respondentId)
│
└── Returns Session {
responses: QuestionResponse[] ──────┐
} │
▼
┌──────────────────────────────────┐
│ QuestionResponse (per question) │
│ │
│ .videoUrl → source video │
│ .silenceRanges → SilenceRange[]
│ .fillerRanges → FillerRange[]│
│ .transcript → { │
│ raw: { segments[] } │
│ corrected?: { segments[] } │
│ } │
│ .metadataForBrowser → { │
│ width, height, │
│ fps, durationInSeconds │
│ } │
│ .question → prompt text │
│ .questionId → unique ID │
└──────────────────────────────────┘
Data Origin Chain
Respondent records video
→ Uploaded to cloud storage (Vercel Blob)
→ Transcribed (Whisper speech-to-text)
→ Silence detected (audio analysis)
→ Filler words detected (transcript analysis)
→ [Optional] AI transcript correction (using CompanyContext)
→ All stored in Redis via VideoProcessingRegistry
→ Retrieved during export
What Each Data Field Feeds Into
| Source Field | Used By | Purpose |
|---|---|---|
response.videoUrl | useVideoDownloader | Downloads the actual video file |
response.silenceRanges | xml-export.ts, TimingCoordinator | Determines which parts to cut from timeline |
response.fillerRanges | xml-export.ts, TimingCoordinator | Additional cuts (um, uh, etc.) |
response.transcript.corrected | VideoExportService.generateCombinedSrtFile() | Preferred caption text (AI-improved) |
response.transcript.raw | VideoExportService.generateCombinedSrtFile() | Fallback caption text (word-level) |
response.metadataForBrowser.fps | xml-export.ts | Frame rate for timeline calculations |
response.metadataForBrowser.width/height | xml-export.ts | Resolution in XML metadata |
response.metadataForBrowser.durationInSeconds | xml-export.ts, TimingCoordinator | Clip duration for timeline positioning |
4. What the Code Does to the Data
4.1 XML Generation (xml-export.ts)
The XML generator creates an Adobe Premiere Pro XMEML v4 project file.
Clip Segmentation (Silence/Filler Removal)
Input: VideoProcessingResult with silenceRanges + fillerRanges
│
▼
convertToTimelineClips()
│
┌───────────────┴───────────────┐
│ For each video clip: │
│ │
│ 1. Combine silence + filler │
│ ranges into one list │
│ 2. Sort by start time │
│ 3. Walk through the clip: │
│ • Content before removal │
│ → new TimelineClip │
│ • Skip removal range │
│ • Content after removal │
│ → new TimelineClip │
│ 4. Each segment gets: │
│ - timeline position │
│ (cumulative, no gaps) │
│ - media in/out points │
│ (original positions) │
└───────────────────────────────┘
Example:
Original clip: 0s ──────────────────────── 60s
Silence: 10s ────── 15s
Filler: 25s ── 27s
Result segments:
Segment 1: timeline 0-10s, media 0-10s
Segment 2: timeline 10-20s, media 15-25s
Segment 3: timeline 20-53s, media 27-60s
(Total timeline: 53s instead of 60s)
XML Structure Generated
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xmeml>
<xmeml version="4">
<sequence>
<uuid>...</uuid>
<duration>[total frames]</duration>
<rate><timebase>30</timebase><ntsc>TRUE</ntsc></rate>
<name>Sequence 01</name>
<media>
<video>
<format>
<!-- 1920x1080, ProRes 422, 30fps -->
</format>
<track>
<!-- All video segments as clipitems -->
<clipitem id="clipitem-1">
<start>[timeline start frame]</start>
<end>[timeline end frame]</end>
<in>[media in frame]</in>
<out>[media out frame]</out>
<file id="file-1">
<pathurl>videos/video_1.mp4</pathurl>
</file>
<link><!-- video ↔ audio linking --></link>
</clipitem>
...
</track>
</video>
<audio>
<track><!-- Audio track 1 (left) --></track>
<track><!-- Audio track 2 (right) --></track>
</audio>
</media>
</sequence>
</xmeml>
4.2 SRT Generation (video-export-service.ts + timing-coordinator.ts)
For each VideoProcessingResult clip:
│
├── Has corrected transcript?
│ ├── YES → Use corrected segments (AI-improved text)
│ │ Map each segment → { start, end, captionString }
│ │ Adjust timestamps via TimingCoordinator
│ │
│ └── NO → Use raw transcript word-level data
│ Flatten segments → individual words
│ Process via TimingCoordinator.filterAndAdjustCaptions()
│
├── Offset captions to cumulative timeline position
│ (so clip 2 captions start after clip 1 ends)
│
└── Advance cumulativeTimelinePosition by clip's
post-silence-removal duration
TimingCoordinator Logic
The TimingCoordinator handles 6 relationships between captions and silence:
Caption: ████████
Silence: ░░░░░░░░
1. BEFORE: ████████ → Keep as-is
░░░░░░░░
2. AFTER: ████████ → Shift left by silence duration
░░░░░░░░
3. OVERLAPS_START: ████████ → Trim end to silence start
░░░░░░░░
4. OVERLAPS_END: ████████ → Move start to silence end
░░░░░░░░
5. SPANS: ██████████████████████ → Split into before + after
░░░░░░░░
6. WITHIN: ████ → Remove entirely
░░░░░░░░
4.3 ZIP Packaging (useVideoDownloader.ts)
Browser-side packaging:
│
├── videos/ ← actual video blobs from parallel fetch
├── project_*.xml ← XMEML from server
├── combined_srt.srt ← SRT from server
├── corrections.txt ← AI corrections report (if available)
└── README.txt ← setup instructions
│
└── Download as .zip via browser Blob URL
5. Current State of the Effort
Code Maturity
| Aspect | Status | Notes |
|---|---|---|
| XML generation | Functional | 697 lines, generates valid XMEML v4 |
| Silence removal in XML | Fixed (was broken) | Archived kanban task confirms this was restored |
| Filler removal in XML | Implemented | Combined with silence ranges |
| SRT generation | Functional | Handles both corrected and raw transcripts |
| Timing coordination | Well-tested | 480 lines of unit tests |
| AI transcript correction | Integrated | Uses CompanyContext when available |
| ZIP packaging | Functional | Browser-side via JSZip |
| Video downloading | Functional | Parallel fetch |
Code Duplication (Technical Debt)
The entire export pipeline is duplicated between admin and library apps:
| File | Admin | Library | Identical? |
|---|---|---|---|
xml-export.ts | Yes (697 lines) | Yes (697 lines) | Yes |
VideoProjectComposer.tsx | Yes | Yes | Yes |
useVideoDownloader.ts | Yes | Yes | Yes |
video-export-service.ts | Yes | Yes | Yes |
timing-coordinator.ts | Yes | Yes | Yes |
srt.ts | Yes | Yes | Yes |
export-xml/route.ts | Yes | Yes | Yes |
There is a P3 backlog kanban task to consolidate these (consolidate-xml-export-utils).
Entry Points
| App | Page | Status |
|---|---|---|
| Admin | /video-exporter?videoFlowId=X&respondentId=Y&source=admin | Active |
| Library | No page exists | Not wired up |
The library app has all the code but no route/page to access it.
6. Recent Changes
Commit History
| Date | Commit | Description |
|---|---|---|
| 2026-01-28 | eccab9f | Initial creation — all export code added as part of "24h product overview (#777)" |
| 2026-01-30 | 34ef31b | Sentry error fixes — touched VideoProjectComposer.tsx only |
Kanban History
| Task | Status | Description |
|---|---|---|
fix-xml-export-premiere-silence-cuts | Archived (resolved) | Silence removal was broken, now fixed |
consolidate-xml-export-utils | Backlog (P3) | Move duplicated code to shared package |
consolidate-raw-clips-export-hook | Backlog (P3) | Move duplicated download hook to shared package |
The code is relatively new (2 weeks old) with minimal changes since initial creation.
7. Risk Assessment — Will It Still Work?
Likely Still Working
- Basic XML structure: XMEML v4 is well-established and Premiere Pro has stable support
- SRT generation: The timing logic is well-tested (480 lines of tests)
- Video download + ZIP packaging: Standard browser APIs, unlikely to break
- Silence/filler removal: Core logic is sound with proper segmentation
Identified Risks and Issues
CRITICAL: Clip Linking Bug for >7 Video Segments
File: apps/library/lib/video-editing/xml-export.ts:469
The audio clip ID calculation uses hardcoded offsets:
// Audio track 1: index + 1 + 7 + 0 = index + 8
// Audio track 2: index + 1 + 7 + 8 = index + 16
And link generation assumes:
// Video → Audio T1: clipIndex + 7
// Video → Audio T2: clipIndex + 15
This means:
- Video clips get IDs:
clipitem-1throughclipitem-N - Audio T1 clips get IDs:
clipitem-8throughclipitem-(N+7) - Audio T2 clips get IDs:
clipitem-16throughclipitem-(N+15)
If N > 7, video clip IDs overlap with audio track 1 IDs, causing Premiere Pro to mislink clips or fail to import.
With silence/filler splitting, even 3-4 source videos can easily produce 8+ segments.
HIGH: Captions Are NOT in the XML Timeline
The SRT file is a separate file in the ZIP. Premiere Pro does not automatically associate an SRT with an XMEML import. The video editor must:
- Import the XML (creates timeline)
- Separately import the SRT file
- Manually place/sync the captions on the timeline
For the goal of "correct captions in Premiere", this workflow is suboptimal. Options:
- Embed captions as a subtitle/text track in the XMEML
- Or provide a
.prprojfile with captions baked in (complex) - Or accept the 2-step import and document it clearly
MEDIUM: Duplicate <file> Elements with Same ID
In generateTimelineClipEnhanced() (line 604), every clipitem contains a full <file> element. When multiple segments reference the same source video, they all emit <file id="file-1"> with full metadata. Premiere Pro may handle this (it deduplicates by ID), but it generates unnecessarily large XML files and some Premiere versions may warn about duplicate file definitions.
The first occurrence should have the full <file> element; subsequent ones should use <file id="file-1"/> (self-closing reference).
MEDIUM: Frame Rate / NTSC Mismatch
The code sets <ntsc>TRUE</ntsc> universally (line 305), which implies 29.97fps drop-frame. But the timebase is set to the source frame rate (often 30fps exactly, or 25fps for European content). This mismatch could cause:
- Slight drift over long timelines
- Incorrect timecode display in Premiere
For 25fps PAL content, NTSC should be FALSE.
LOW: pproTicksIn Calculation Assumes Fixed Tick Rate
Line 595: <pproTicksIn>${inFrames * 8475667200}</pproTicksIn>
The value 8475667200 is the tick count per frame at 29.97fps (254,016,000,000 ticks/second ÷ 29.97 fps). But if the actual frame rate is 25fps or 24fps, this calculation would be wrong. The ticks-per-frame should be 254016000000 / frameRate.
LOW: Missing exportSuccess State Update
In VideoProjectComposer.tsx, setExportSuccess(true) is never called in the handleExport try block — the state is initialized as false but never set to true on success, so the success alert never shows.
8. Identified Issues — Summary
| # | Severity | Issue | File:Line | Impact |
|---|---|---|---|---|
| 1 | CRITICAL | Clip ID collision when >7 segments | xml-export.ts:469 | Broken audio linking in Premiere |
| 2 | HIGH | Captions not embedded in XML | xml-export.ts (entire) | Editor must manually import SRT |
| 3 | MEDIUM | Duplicate <file> elements | xml-export.ts:604 | Large XML, potential warnings |
| 4 | MEDIUM | NTSC=TRUE hardcoded for all frame rates | xml-export.ts:305 | Timecode drift for non-NTSC content |
| 5 | LOW | pproTicksIn uses fixed tick rate | xml-export.ts:595 | Slight timing inaccuracy |
| 6 | LOW | exportSuccess never set to true | VideoProjectComposer.tsx:87 | Success message never shows |
| 7 | TECH DEBT | Code duplicated across admin/library | All export files | Maintenance burden |
9. Recommendations for Correct Captions in Premiere
Option A: Embed Captions in XMEML as a Text/Generator Track (Recommended)
Add a subtitle track to the XML timeline using Premiere's "Captions" track type. XMEML v4 supports <generatoritem> elements that can represent text overlays. This would give editors captions directly on the timeline when they import the XML.
Pros: Single import, captions are timeline-synced Cons: Complex XML generation, limited styling control
Option B: Switch to Premiere Pro's Native Caption Import Format
Instead of (or in addition to) SRT, generate a .scc (Scenarist Closed Caption) or .mcc file that Premiere Pro natively imports as a captions track. These formats have better Premiere integration than SRT.
Pros: Better Premiere integration Cons: Different format to generate, less universal
Option C: Keep SRT but Improve the Workflow Documentation
Accept the 2-step import (XML + SRT) and provide clear instructions:
- Import XML
- File → Import → select SRT file
- Drag SRT to timeline's caption track
Also consider renaming the SRT filename to match the XML project name for editor convenience.
Pros: Simplest to implement Cons: Manual step for every export
Option D: Switch to Premiere Pro Project Format (.prproj)
Generate a native .prproj file (which is a compressed XML format). This would include everything — timeline, media references, and captions — in one file.
Pros: Complete solution, best editor experience Cons: Complex, reverse-engineered format, fragile across Premiere versions
10. File Reference
Core Export Files
| File | Path | Lines | Purpose |
|---|---|---|---|
| XML Generator | apps/library/lib/video-editing/xml-export.ts | 698 | XMEML v4 generation with silence removal |
| Export Service | apps/library/lib/services/video-export-service.ts | 242 | Orchestrates XML + SRT generation |
| Timing Coordinator | apps/library/lib/video-editing/timing-coordinator.ts | 279 | Adjusts caption timing for silence removal |
| SRT Generator | apps/library/lib/utils/srt.ts | 92 | Formats captions as SRT |
| Video Downloader | apps/library/hooks/useVideoDownloader.ts | 230 | Downloads videos + creates ZIP |
| UI Component | apps/library/components/features/video-editing/VideoProjectComposer.tsx | 257 | Export UI with progress states |
| API Route | apps/library/app/api/export-xml/route.ts | 51 | POST endpoint for export |
| Admin Page | apps/admin/app/video-exporter/page.tsx | 216 | Entry point (admin only) |
Type Definitions
| Type | Path | Purpose |
|---|---|---|
VideoProcessingResult | packages/video/src/types/video-editing.ts | Core data structure for export |
ExportRequest/Response | packages/video/src/types/export.ts | API contract |
QuestionResponse | packages/registries/src/server/video-processing-types.ts | Registry data shape |
CompanyContext | packages/video/src/types/transcription.ts | AI correction context |
Caption, TranscriptionWord | packages/video/src/types/transcription.ts | Caption/word types |
Tests
| Test | Path | Coverage |
|---|---|---|
| TimingCoordinator | apps/library/lib/video-editing/__tests__/timing-coordinator.test.ts | 480 lines, comprehensive |
| XML Export | None | No tests exist |
| SRT Generation | None | No tests exist |
Kanban Tasks
| Task | Status | Path |
|---|---|---|
| Fix silence cuts | Archived (done) | .kanbn/archived-tasks/fix-xml-export-premiere-silence-cuts.md |
| Consolidate XML utils | Backlog (P3) | .kanbn/tasks/consolidate-xml-export-utils.md |
| Consolidate export hook | Backlog (P3) | .kanbn/tasks/consolidate-raw-clips-export-hook.md |