From 6fcb46f1af92a86f97acf2e52c1c930903e43a31 Mon Sep 17 00:00:00 2001 From: Andrew Ford Date: Sat, 30 May 2026 01:47:28 -0700 Subject: [PATCH] Support audio and image footage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ButterCut treated every clip as a video file with both a video and an audio stream. Bring audio (music, voiceover) and still images in as first-class footage: import them into a library, place them on the single timeline track, and export to Final Cut, Premiere, and Resolve. A per-clip media_type (video | audio | image) drives it. The library infers it from the file extension at import; the exporter threads it through to the generators, which require it and emit the right timeline element per kind. One discriminator covers what audio and images break — images carry no intrinsic duration, audio has no video stream, and images have no audio stream. - Library: stamp media_type on every clip, synthesize a duration for stills, and judge readiness per kind (audio and images need only a summary; audio's transcript is optional). - Processing: build contact sheets for video only; summarize audio from its transcript and images from the image; skip stills in transcription. - Export: require and validate media_type, fall back to sane defaults when a stream is absent, and give images a default 5s on-timeline duration (overridable per cut). FCP7 emits audio-only and still clipitems with correct per-track link indices; FCPX sets hasVideo/hasAudio per kind. Timelines stay strictly single-track: an audio or image clip takes its own slot and plays only during it. Migrate existing libraries with scripts/005_migrate_add_media_type.rb, which stamps every pre-existing clip media_type: video (idempotent). 249 specs pass; FCPX output validates against FCPXMLv1_8.dtd. --- AGENTS.md | 11 +- CHANGELOG.md | 10 ++ docs/basic-xml-generation.md | 38 ------ docs/creating-a-cut.md | 4 - docs/creating-a-library.md | 14 +- docs/example-library-setup.md | 6 +- docs/usage.md | 1 - lib/buttercut.rb | 11 +- lib/buttercut/editor_base.rb | 82 +++++++++-- lib/buttercut/export.rb | 50 +++++-- lib/buttercut/fcp7.rb | 41 ++++-- lib/buttercut/fcpx.rb | 17 ++- lib/buttercut/footage_processor.rb | 17 ++- lib/buttercut/library.md | 6 +- lib/buttercut/library.rb | 76 +++++++++-- lib/buttercut/premiere.rb | 4 +- lib/buttercut/rotation_metadata.rb | 2 + scripts/005_migrate_add_media_type.rb | 148 ++++++++++++++++++++ skills/analyze-video/SKILL.md | 20 ++- skills/analyze-video/agent_prompt.md | 34 +++-- skills/create-library/SKILL.md | 8 +- skills/cut/cut_yaml_schema.md | 15 ++- skills/cut/roughcut_agent_prompt.md | 11 +- skills/transcribe-audio/agent_prompt.md | 6 +- spec/buttercut/fcp7_spec.rb | 6 +- spec/buttercut/fcpx_spec.rb | 59 ++++---- spec/buttercut/library_spec.rb | 92 +++++++++++++ spec/buttercut/media_types_spec.rb | 172 ++++++++++++++++++++++++ spec/buttercut/premiere_spec.rb | 18 +-- spec/buttercut_spec.rb | 2 +- spec/migrations_spec.rb | 151 ++++++++++++++++++++- spec/video_metadata_spec.rb | 2 +- templates/library_template.yaml | 11 +- 33 files changed, 965 insertions(+), 180 deletions(-) delete mode 100644 docs/basic-xml-generation.md create mode 100644 scripts/005_migrate_add_media_type.rb create mode 100644 spec/buttercut/media_types_spec.rb diff --git a/AGENTS.md b/AGENTS.md index b8348c4..75912d4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,9 @@ The ButterCut folder is one project with two parts: ## Core Workflow -You help with video tasks by processing raw video footage by analyzing transcripts and indexing visuals through libraries. +Claude/Codex/The Agent/You are working as an assistant AI video editor working for a (non-technical, non-engineer) video editor. + +You help with video tasks by processing raw footage — video, plus any audio and still images — analyzing transcripts and indexing visuals through libraries. Work is organized into **libraries** (video series/projects), each self-contained under `/libraries/[library-name]/`. When a user refers to a library, you you'll want to load the library file in memory. If they talk about building a roughcut, extracting dialogue, etc, you'll need to first find and read the correct library file. If it's not clear what library they're talking about, find recently modified libraries and list them for the user using the AskUserQuestionTool or similiar to see what library they want to work with. If it's clear what library they're referring to, just start working with that library. @@ -42,6 +44,7 @@ Known migration triggers (match each to a `scripts/NNN_migrate_*.rb` script via - video entries with `summary` missing (added in 0.5.0; missing means "todo", default to empty string) - video entries with `transcript_path` / `visual_transcript_path` (renamed to `transcript` / `visual_transcript` in 0.3.0) - video entries with `file_size_mb` (removed in 0.3.0) +- video entries with `media_type` missing (added in 0.8.0; missing means "predates audio/image support, default to `video`") - library has a `roughcuts/` directory (renamed to `cuts/` when the `roughcut` skill became `cut`). This trigger is layout, not YAML — check the directory listing, not the schema. A missing field is not the same as a field set to the template default — the template default only applies to freshly created libraries. If you see a schema issue not on this list, still check CHANGELOG.md; the list may be behind. After running migrations, re-read the library.yaml and continue with whatever the user asked for. @@ -89,7 +92,7 @@ Writes (`add_videos`, `complete`, `update_metadata`), destructive resets, legacy ## Key Reminders - After exporting an XML file, offer to open it directly in the user's editor with `open -a "Final Cut Pro"`, `open -a "Adobe Premiere Pro"`, or `open -a "DaVinci Resolve"` (matching the library's `editor` setting). Check `libraries/settings.yaml` for `open_in_editor_after_export` — if the key is missing, ask and save the preference. If `open -a` fails with "Unable to find application named ...", don't assume the editor is missing — the app may be installed under a slightly different name (e.g. a version suffix). Grep the installed apps for it (`ls /Applications | grep -i premiere`, or `mdfind "kMDItemKind == 'Application'" | grep -i resolve`) and retry `open -a` with the actual name before telling the user it isn't installed. -- Never modify source video files - always preserve originals +- Never modify source media files (video, audio, or images) - always preserve originals - Flag areas needing human judgment rather than making assumptions - When possible, use the existing Ruby files to get work done. Make scripts when the skill or step doesn't provide what you need. - Parallelism caps live in each skill's `SKILL.md` (parent brief). Read it before dispatching sub agents. @@ -106,9 +109,9 @@ Writes (`add_videos`, `complete`, `update_metadata`), destructive resets, legacy ButterCut is designed to be geared toward working with non technical people using ButterCut via a client, Claude Cowork or Claude Code. -- **Input**: Array of full file paths to video files +- **Input**: Array of full file paths to footage files (video, audio, or still images) - **Output**: Working XML file ready to import into the non-technical user's video editor (Final Cut, Premiere, Resolve) -- **Metadata Extraction**: Uses FFmpeg internally to extract video properties (duration, resolution, frame rate, audio rate, etc.) +- **Metadata Extraction**: Uses FFmpeg internally to extract media properties (duration, resolution, frame rate, audio rate, etc.) ### Vocabulary — talk like an editor, not a developer diff --git a/CHANGELOG.md b/CHANGELOG.md index 14edbd5..dca5491 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- **Audio and image footage (single-track).** Libraries can now import audio files (`.mp3/.wav/.m4a/.aac/.flac/.ogg`) and still images (`.jpg/.png/.heic/...`) alongside video. A new per-clip `media_type` field (`video | audio | image`, inferred from the file extension at add time) routes each clip through the right analysis: audio gets a transcript + summary (no contact sheet), images get a summary written from the image itself (no transcript, no contact sheet), video is unchanged. All three export onto the single timeline track in Final Cut, Premiere, and Resolve — audio clips emit an audio-only element, stills emit a video-only element with a default 5s duration (overridable per-clip in the cut). Staying single-track, a music/voiceover clip plays only during its own slot; there is no second track for music *under* video. +- **Daily update-check gate in the `Library` CLI.** Library commands now check a gitignored `last_buttercut_update_check` stamp first; once a day they raise `UpdateCheckNeeded`, surfaced as a `library:` error that tells the agent to check for a newer ButterCut and offer to update. + +### Migration +- **`media_type` added to every clip (0.8.0).** Existing video entries are stamped `media_type: video`. Run: + ```bash + ruby lib/buttercut/library.rb migrate + ``` + ## [0.7.2] - 2026-06-02 Processing your footage now uses less of your account, your Mac stays awake while it works, and we've fixed a bug in Premiere files so vertical clips export to Premiere right side up. diff --git a/docs/basic-xml-generation.md b/docs/basic-xml-generation.md deleted file mode 100644 index 93f8f37..0000000 --- a/docs/basic-xml-generation.md +++ /dev/null @@ -1,38 +0,0 @@ -# Basic XML Generation - -From a script at the root of your ButterCut clone: - -```ruby -require_relative 'lib/buttercut' - -# Create a 3-clip timeline with 3 seconds from each video -videos = [ - { path: '/path/to/video1.mp4', duration: 3.0 }, - { path: '/path/to/video2.mp4', duration: 3.0, start_at: 30.0 }, - { path: '/path/to/video3.mp4', duration: 3.0, start_at: 2.0 } -] - -# Final Cut Pro X timeline -fcpx_generator = ButterCut.new(videos, editor: :fcpx) -fcpx_generator.save('timeline.fcpxml') - -# Final Cut Pro 7 / Adobe Premiere / DaVinci Resolve timeline -fcp7_generator = ButterCut.new(videos, editor: :fcp7) -fcp7_generator.save('timeline.xml') -``` - -## Clip Options - -Each clip in the array is a hash with the following keys: - -- **`path`** (required): Absolute path to the video file -- **`start_at`** (optional): Where in the source file to start reading (default: 0.0) - - Trims the beginning of the video - - Specified as seconds (float) - - Examples: `2.0` (2 seconds), `1.5` (1.5 seconds) - - Automatically rounded to nearest frame boundary for precision -- **`duration`** (optional): How much of the source to use (default: full video from start_at) - - Specified as seconds (float) - - Examples: `5.0` (5 seconds), `3.5` (3.5 seconds) - - If not specified and `start_at` is provided, uses remaining video after trim - - Automatically rounded to nearest frame boundary for precision diff --git a/docs/creating-a-cut.md b/docs/creating-a-cut.md index 5a3ffc0..9832286 100644 --- a/docs/creating-a-cut.md +++ b/docs/creating-a-cut.md @@ -52,7 +52,3 @@ Every cut exports a timeline into `libraries/[library-name]/cuts/`: - **DaVinci Resolve** → `.xml` If you've enabled it, Claude can also drop a copy on your Desktop and open the file directly in your editor after export. - -## Direct XML generation - -To generate a timeline in Ruby without going through Claude, see [basic-xml-generation.md](basic-xml-generation.md). diff --git a/docs/creating-a-library.md b/docs/creating-a-library.md index 8bff87b..0572e7a 100644 --- a/docs/creating-a-library.md +++ b/docs/creating-a-library.md @@ -1,6 +1,6 @@ -# Creating a Video Library +# Creating a Library -A **library** organizes your video footage along with the transcripts, contact sheets, and summaries Claude needs to edit it. Creating one is the first thing you do with ButterCut — you can't build a cut until a library is processed. +A **library** organizes your footage — video, plus any audio (music/voiceover) and still images — along with the transcripts, contact sheets, and summaries Claude needs to edit it. Creating one is the first thing you do with ButterCut — you can't build a cut until a library is processed. Just tell Claude you want a new library and it walks you through setup: @@ -11,18 +11,20 @@ Claude: [Guides you through library setup and asks for details] You: - Library name: "wedding" - - Video location: "/path/to/videos" + - Footage location: "/path/to/footage" - Language: "English" -Claude: [Automatically processes all videos] +Claude: [Automatically processes all footage] ✓ Creates the library structure - ✓ Transcribes audio with word-level timing (WhisperX) - ✓ Builds a contact sheet for every clip + ✓ Transcribes audio with word-level timing (WhisperX) — video and audio clips + ✓ Builds a contact sheet for every video clip ✓ Writes a short summary of each clip Result: Full footage analysis, ready to build a cut ``` +Video, audio, and still images can all live in one library on a single track. ButterCut infers each clip's type from its file extension and routes it: video gets all three steps, audio (music/voiceover) gets a transcript and summary, and an image gets just a summary (no contact sheet). + Claude handles parallel processing, metadata extraction (via FFmpeg), and analysis. Each library is self-contained under `libraries/[library-name]/`, with `library.yaml` as its source of truth. ## Adding footage or resuming diff --git a/docs/example-library-setup.md b/docs/example-library-setup.md index ab5d034..a405dde 100644 --- a/docs/example-library-setup.md +++ b/docs/example-library-setup.md @@ -2,6 +2,8 @@ This walkthrough shows a complete example of using ButterCut with Claude Code to analyze wedding footage. The entire process—from initial request to fully analyzed footage—happens in one conversation with automatic parallel processing. +> This example library is all video. ButterCut also accepts audio (music/voiceover) and still images — they're added and analyzed the same way, each routed by its file type. + --- ## The Conversation @@ -14,7 +16,7 @@ I'll help you create a new library. Let me gather some information about your pr **Questions:** 1. What would you like to name this library? -2. Where are your video files located? +2. Where is your footage located? 3. What language is spoken in these videos? ### **User** @@ -23,7 +25,7 @@ I'll help you create a new library. Let me gather some information about your pr ### **Claude** Perfect! The library will be named "wedding". -Now, what is the directory path containing your video files? +Now, what is the directory path containing your footage? ### **User** > /Users/andrew/Final Cuts/Wedding Camcorder Footage diff --git a/docs/usage.md b/docs/usage.md index e762c12..261dbdb 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -5,4 +5,3 @@ First tell Claude to create a **library** — it organizes your footage along wi - [Installation](installation.md) — manual setup and dependency versions. - [Creating a library](creating-a-library.md) — process footage into a library. ([full walkthrough](example-library-setup.md)) - [Creating a cut](creating-a-cut.md) — build a scene, selects reel, roughcut, or custom cut and export it. -- [Basic XML generation](basic-xml-generation.md) — generate timelines in Ruby without Claude Code. diff --git a/lib/buttercut.rb b/lib/buttercut.rb index 561fc45..a668e94 100644 --- a/lib/buttercut.rb +++ b/lib/buttercut.rb @@ -15,10 +15,15 @@ # FCP7 itself is the shared xmeml-v5 format base that Resolve and Premiere subclass; # it is not a public editor symbol. # -# Example usage: +# Clips are built by lib/buttercut/export.rb — the only caller — from a cut's +# YAML against its library. Each clip hash carries a media_type +# (video | audio | image) so the generators route it onto the single timeline +# track; every clip must declare one (the constructor requires it). video/audio +# trim with start_at + duration; an image just takes an on-timeline duration: # clips = [ -# { path: '/absolute/path/to/video1.mov', start_at: 2.0, duration: 5.0 }, -# { path: '/absolute/path/to/video2.mov' } +# { path: '/abs/interview.mov', start_at: 2.0, duration: 5.0, media_type: 'video' }, +# { path: '/abs/music.mp3', start_at: 0.0, duration: 8.0, media_type: 'audio' }, +# { path: '/abs/still.jpg', start_at: 0.0, duration: 5.0, media_type: 'image' } # ] # generator = ButterCut.new(clips, editor: :fcpx) # generator.save('output.fcpxml') diff --git a/lib/buttercut/editor_base.rb b/lib/buttercut/editor_base.rb index 0a93e9f..4cc49b2 100644 --- a/lib/buttercut/editor_base.rb +++ b/lib/buttercut/editor_base.rb @@ -14,6 +14,26 @@ class EditorBase DEFAULT_INITIAL_OFFSET = "0s" DEFAULT_VOLUME_ADJUSTMENT = "-13.100000000000001db" + # Fallbacks for clips that lack a stream the timeline math expects: audio + # files have no video stream (no dimensions/frame rate), images have no + # audio stream. The values are only ever used to keep the fraction math and + # the sequence-format header well-formed — they never override a real clip's + # own metadata, and an audio/image clip emits no element that reads them. + DEFAULT_WIDTH = 1920 + DEFAULT_HEIGHT = 1080 + DEFAULT_FRAME_RATE = "30/1" + DEFAULT_SAMPLE_RATE = "48000" + + # Stills have no source duration of their own; FCP7/FCPXML treat them as + # effectively unbounded media. Give an image asset a generous source + # duration so any chosen on-timeline length fits inside it. + IMAGE_ASSET_DURATION_SECONDS = 3600 + + # The media kinds a clip may declare. The exporter (the only caller) stamps + # one on every clip from library.yaml, and the constructor requires it — so + # the generators read the kind directly instead of guessing a default. + MEDIA_TYPES = %w[video audio image].freeze + attr_reader :clips, :initial_offset, :volume_adjustment def initialize(clips) @@ -34,6 +54,12 @@ def initialize(clips) raise ArgumentError, "All video file paths must be absolute paths. Relative paths found: #{paths}" end + clips.each_with_index do |clip, index| + next if MEDIA_TYPES.include?(clip[:media_type]) + + raise ArgumentError, "Clip at index #{index} must have a media_type of #{MEDIA_TYPES.join(', ')}, got #{clip[:media_type].inspect}" + end + @clips = clips @initial_offset = DEFAULT_INITIAL_OFFSET @volume_adjustment = DEFAULT_VOLUME_ADJUSTMENT @@ -65,12 +91,16 @@ def audio_stream(video_path) extract_metadata(video_path)['streams'].find { |s| s['codec_type'] == 'audio' } end + # A clip's media kind (video|audio|image). The constructor requires every + # clip to declare one, so this is a plain read with no default to guess. + def clip_kind(clip) = clip[:media_type] + def video_width(video_path) - video_stream(video_path)['width'] + video_stream(video_path)&.dig('width') || DEFAULT_WIDTH end def video_height(video_path) - video_stream(video_path)['height'] + video_stream(video_path)&.dig('height') || DEFAULT_HEIGHT end # Degrees clockwise (0/90/180/270) the source must be rotated to display upright, @@ -85,7 +115,12 @@ def video_duration(video_path) end def frame_rate(video_path) - video_stream(video_path)['r_frame_rate'] + rate = video_stream(video_path)&.dig('r_frame_rate') + # No stream, or a degenerate rate (stills can report "0/0" or "N/D"): use a + # sane fallback so the fraction math downstream never divides by zero. + return DEFAULT_FRAME_RATE if rate.nil? || rate.start_with?('0/') || rate.end_with?('/0') + + rate end def frame_duration(video_path) @@ -95,7 +130,7 @@ def frame_duration(video_path) end def audio_sample_rate(video_path) - audio_stream(video_path)['sample_rate'] + audio_stream(video_path)&.dig('sample_rate') || DEFAULT_SAMPLE_RATE end def nominal_frame_rate(video_path) @@ -206,32 +241,43 @@ def duration_to_fraction(video_path) "#{duration_num / divisor}/#{duration_denom / divisor}s" end + # The clip whose metadata defines the sequence format. Prefer a real video + # clip; fall back to an image (a stills-only timeline still needs real + # dimensions); finally the first clip (an all-audio timeline, where the + # dimensions are a harmless placeholder no one sees). + def format_source_path + @format_source_path ||= + (@clips.find { |c| clip_kind(c) == 'video' } || + @clips.find { |c| clip_kind(c) == 'image' } || + @clips.first)[:path] + end + def format_width - video_width(@clips.first[:path]) + video_width(format_source_path) end def format_height - video_height(@clips.first[:path]) + video_height(format_source_path) end def format_frame_duration - frame_duration(@clips.first[:path]) + frame_duration(format_source_path) end def format_frame_rate - frame_rate(@clips.first[:path]) + frame_rate(format_source_path) end def format_nominal_frame_rate - nominal_frame_rate(@clips.first[:path]) + nominal_frame_rate(format_source_path) end def format_color_space - color_space(@clips.first[:path]) + color_space(format_source_path) end def format_audio_rate - audio_sample_rate(@clips.first[:path]) + audio_sample_rate(format_source_path) end # Greatest Common Divisor (GCD): the largest whole number that divides two @@ -358,6 +404,7 @@ def build_asset_map abs_path = get_absolute_path(video_file_path) next if file_to_asset.key?(abs_path) + kind = clip_kind(clip_def) asset_id = deterministic_asset_id(abs_path) asset_uid = deterministic_asset_uid(abs_path) filename = get_filename(video_file_path) @@ -370,7 +417,9 @@ def build_asset_map filename: filename, basename: get_basename(filename), file_url: file_url, - asset_duration: duration_to_fraction(video_file_path), + has_video: kind != 'audio', + has_audio: kind != 'image', + asset_duration: asset_duration_fraction(video_file_path, kind), audio_rate: audio_sample_rate(video_file_path), timecode: clip_timecode_fraction(video_file_path), frame_duration: frame_duration(video_file_path), @@ -384,6 +433,15 @@ def build_asset_map file_to_asset end + # A still has no source duration of its own (`video_duration` reads 0), so + # give it a bounded placeholder that comfortably exceeds any on-timeline + # length. Video and audio carry a real duration. + def asset_duration_fraction(path, kind) + return seconds_to_fraction(IMAGE_ASSET_DURATION_SECONDS) if kind == 'image' + + duration_to_fraction(path) + end + def build_timeline_clips(asset_map, timeline_frame_duration) current_offset = initial_offset clips = @clips.map do |clip_def| diff --git a/lib/buttercut/export.rb b/lib/buttercut/export.rb index 58034d1..f8be5a7 100755 --- a/lib/buttercut/export.rb +++ b/lib/buttercut/export.rb @@ -27,11 +27,15 @@ def initialize(roughcut_path:, output_path:, editor:) @editor = resolve_editor(editor.to_s) end + # A still has no intrinsic duration; this is how long it sits on the timeline + # unless the clip overrides it. + DEFAULT_IMAGE_DURATION_SECONDS = 5.0 + def perform roughcut = load_yaml(@roughcut_path) library = load_library(@roughcut_path) - video_paths = index_video_paths(library) - clips = build_clips(roughcut, video_paths) + video_index = index_videos(library) + clips = build_clips(roughcut, video_index) write_xml(clips, @editor) validate_fcpxml(@output_path) if @editor == :fcpx @@ -56,27 +60,57 @@ def load_library(roughcut_path) load_yaml(library_yaml) end - def index_video_paths(library) + def index_videos(library) library['videos'].each_with_object({}) do |video, map| - map[File.basename(video['path'])] = video['path'] + map[File.basename(video['path'])] = { + path: video['path'], + media_type: video['media_type'] || 'video' + } end end - def build_clips(roughcut, video_paths) + def build_clips(roughcut, video_index) roughcut['clips'].filter_map do |clip| source = clip['source_file'] - path = video_paths[source] + entry = video_index[source] - unless path + unless entry warn "Warning: Source file not found in library data: #{source}" next end + clip_for(clip, entry) + end + end + + # Build the generator's clip hash. Video and audio are trimmed with in/out + # points; a still image has no intrinsic timing, so it gets an explicit + # on-timeline duration starting at 0. media_type rides along so the generators + # know which kind of timeline element to emit. + def clip_for(clip, entry) + if entry[:media_type] == 'image' + { path: entry[:path], start_at: 0.0, duration: image_duration_seconds(clip), media_type: 'image' } + else start_at = timecode_to_seconds(clip['in_point']) duration = timecode_to_seconds(clip['out_point']) - start_at + { path: entry[:path], start_at: start_at.to_f, duration: duration.to_f, media_type: entry[:media_type] } + end + end - { path: path, start_at: start_at.to_f, duration: duration.to_f } + # An image clip's on-timeline length: an explicit `duration` (HH:MM:SS.ss or a + # bare number of seconds), else the in/out span if both are given, else the + # default. + def image_duration_seconds(clip) + raw = clip['duration'] + return raw.to_f if raw.is_a?(Numeric) + return timecode_to_seconds(raw).to_f if raw.is_a?(String) && !raw.strip.empty? + + if clip['in_point'] && clip['out_point'] + span = timecode_to_seconds(clip['out_point']) - timecode_to_seconds(clip['in_point']) + return span.to_f if span.positive? end + + DEFAULT_IMAGE_DURATION_SECONDS end # Accepts HH:MM:SS or HH:MM:SS.s diff --git a/lib/buttercut/fcp7.rb b/lib/buttercut/fcp7.rb index 1827f3d..6dcac74 100644 --- a/lib/buttercut/fcp7.rb +++ b/lib/buttercut/fcp7.rb @@ -65,7 +65,7 @@ def to_xml end end xml.track do - clip_payloads.each do |payload| + clip_payloads.select { |payload| payload[:has_video] }.each do |payload| build_video_clipitem(xml, payload) end end @@ -79,7 +79,7 @@ def to_xml end end xml.track do - clip_payloads.each do |payload| + clip_payloads.select { |payload| payload[:has_audio] }.each do |payload| build_audio_clipitem(xml, payload) end end @@ -99,8 +99,20 @@ def ntsc_flag_for(rate_denom) end def build_clip_payloads(timeline_clips, timeline_frame_duration) + # The timeline is one sequential track. FCP7 just represents it as two + # lanes — picture (