buttercut 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: buttercut
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Ford
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-25 00:00:00.000000000 Z
11
+ date: 2026-05-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -24,20 +24,6 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.13'
27
- - !ruby/object:Gem::Dependency
28
- name: rubyzip
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '2.3'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '2.3'
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: rspec
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -62,25 +48,9 @@ extensions: []
62
48
  extra_rdoc_files: []
63
49
  files:
64
50
  - ".claude/commands/worktree.md"
65
- - ".claude/scripts/script_extractor.rb"
66
51
  - ".claude/settings.json"
67
52
  - ".claude/settings.local.json"
68
- - ".claude/skills/analyze-video/SKILL.md"
69
- - ".claude/skills/analyze-video/prepare_visual_script.rb"
70
- - ".claude/skills/backup-library/SKILL.md"
71
- - ".claude/skills/backup-library/backup_libraries.rb"
72
- - ".claude/skills/release/SKILL.md"
73
- - ".claude/skills/roughcut/SKILL.md"
74
- - ".claude/skills/roughcut/agent_instructions.md"
75
- - ".claude/skills/roughcut/export_to_fcpxml.rb"
76
- - ".claude/skills/setup/SKILL.md"
77
- - ".claude/skills/setup/advanced-setup.md"
78
- - ".claude/skills/setup/simple-setup.md"
79
- - ".claude/skills/setup/verify_install.rb"
80
- - ".claude/skills/transcribe-audio/SKILL.md"
81
- - ".claude/skills/transcribe-audio/prepare_audio_script.rb"
82
- - ".claude/skills/transcribe-audio/refine_instructions.md"
83
- - ".claude/skills/update-buttercut/SKILL.md"
53
+ - ".claude/skills"
84
54
  - CLAUDE.md
85
55
  - LICENSE
86
56
  - README.md
@@ -91,11 +61,12 @@ files:
91
61
  - lib/buttercut/fcpx.rb
92
62
  - lib/buttercut/version.rb
93
63
  - templates/library_template.yaml
64
+ - templates/plan_template.md
94
65
  - templates/roughcut_template.yaml
95
66
  - templates/settings_template.yaml
96
67
  homepage: https://github.com/andrewford/buttercut
97
68
  licenses:
98
- - MIT
69
+ - Nonstandard
99
70
  metadata: {}
100
71
  post_install_message:
101
72
  rdoc_options: []
@@ -112,7 +83,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
112
83
  - !ruby/object:Gem::Version
113
84
  version: '0'
114
85
  requirements: []
115
- rubygems_version: 3.5.23
86
+ rubygems_version: 3.5.22
116
87
  signing_key:
117
88
  specification_version: 4
118
89
  summary: Video Editor XML generator with Agent skills for analyzing video, creating
@@ -1,66 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # Extract the plain-text script from a WhisperX-style transcript JSON.
3
- #
4
- # Usage:
5
- # ruby .claude/scripts/script_extractor.rb <transcript.json> <output.txt>
6
- #
7
- # Output is one segment per paragraph (blank line between), trimmed, suitable
8
- # for proofreading by a human or a sub-agent without the overhead of the full
9
- # transcript JSON (word-level timing, scores, etc.).
10
-
11
- require 'json'
12
-
13
- class ScriptExtractor
14
- def self.extract(transcript_path, output_path)
15
- new(transcript_path, output_path).extract
16
- end
17
-
18
- def initialize(transcript_path, output_path)
19
- raise ArgumentError, "transcript_path is required" if transcript_path.nil? || transcript_path.empty?
20
- raise ArgumentError, "output_path is required" if output_path.nil? || output_path.empty?
21
- @transcript_path = transcript_path
22
- @output_path = output_path
23
- end
24
-
25
- def extract
26
- write_output(format_script)
27
- report
28
- end
29
-
30
- private
31
-
32
- attr_reader :transcript_path, :output_path
33
-
34
- def data
35
- @data ||= JSON.parse(File.read(transcript_path))
36
- end
37
-
38
- def segments
39
- data["segments"] or raise "transcript JSON has no 'segments' key: #{transcript_path}"
40
- end
41
-
42
- def format_script
43
- paragraphs = segments.map { |s| s["text"].to_s.strip }.reject(&:empty?)
44
- paragraphs.join("\n\n") + "\n"
45
- end
46
-
47
- def write_output(text)
48
- File.write(output_path, text)
49
- end
50
-
51
- def report
52
- in_kb = (File.size(transcript_path) / 1024.0).round(1)
53
- out_kb = (File.size(output_path) / 1024.0).round(1)
54
- puts "Extracted script: #{output_path} (#{out_kb} KB from #{in_kb} KB source, #{segments.size} segments)"
55
- end
56
- end
57
-
58
- if __FILE__ == $PROGRAM_NAME
59
- transcript_path, output_path = ARGV
60
- abort("usage: script_extractor.rb <transcript.json> <output.txt>") unless transcript_path && output_path
61
- abort("file not found: #{transcript_path}") unless File.file?(transcript_path)
62
- if File.expand_path(output_path) == File.expand_path(transcript_path)
63
- abort("output path must differ from transcript path: #{transcript_path}")
64
- end
65
- ScriptExtractor.extract(transcript_path, output_path)
66
- end
@@ -1,97 +0,0 @@
1
- ---
2
- name: analyze-video
3
- description: Adds visual descriptions to transcripts by extracting and analyzing video frames with ffmpeg. Creates visual transcript with periodic visual descriptions of the video clip. Use when all files have audio transcripts present (transcript) but don't yet have visual transcripts created (visual_transcript).
4
- ---
5
-
6
- # Skill: Analyze Video
7
-
8
- Add visual descriptions to audio transcripts by extracting JPG frames with ffmpeg and analyzing them. **Never read video files directly** - extract frames first.
9
-
10
- ## Prerequisites
11
-
12
- Videos must have audio transcripts. Run **transcribe-audio** skill first if needed.
13
-
14
- ## Workflow
15
-
16
- ### 1. Inputs from the parent
17
-
18
- This skill runs as a sub-agent. Do NOT read `library.yaml` or `settings.yaml` — the parent has that context and passes everything inline in your prompt. Expect these inputs:
19
-
20
- - `video_path` — absolute path to the video file
21
- - `audio_transcript_path` — absolute path to the prepared audio transcript JSON
22
- - `visual_transcript_path` — absolute path to write the visual transcript JSON
23
-
24
- ### 2. Copy & Clean Audio Transcript
25
-
26
- Don't read the audio transcript, just copy it and then prepare it by using the prepare_visual_script.rb file. This removes word-level timing data and prettifies the JSON for easier editing:
27
-
28
- ```bash
29
- cp <audio_transcript_path> <visual_transcript_path>
30
- ruby .claude/skills/analyze-video/prepare_visual_script.rb <visual_transcript_path>
31
- ```
32
-
33
- ### 3. Extract Frames (Binary Search)
34
-
35
- Create frame directory: `mkdir -p tmp/frames/[video_name]`
36
-
37
- **Videos ≤30s:** Extract one frame at 2s
38
- **Videos >30s:** Extract start (2s), middle (duration/2), end (duration-2s)
39
-
40
- ```bash
41
- ffmpeg -ss 00:00:02 -i video.mov -vframes 1 -vf "scale=1280:-1" tmp/frames/[video_name]/start.jpg
42
- ```
43
-
44
- **Subdivide when:** Footage start, middle and end have different subjects, setting or angle changes
45
- **Stop when:** The footage no longer seems to be changing or only has minor changes
46
- **Never sample** more frequently than once per 30 seconds
47
-
48
- ### 4. Add Visual Descriptions
49
-
50
- Read the visual video json file that you created earlier.
51
-
52
- **Read the JPG frames** from `tmp/frames/[video_name]/` using Read tool, then **Edit** the file at `<visual_transcript_path>`:
53
-
54
- Do these incrementally. You don't need to create a program or script to do this, just incrementally edit the json whenever you read new frames.
55
-
56
- **Dialogue segments - add `visual` field:**
57
- ```json
58
- {
59
- "start": 2.917,
60
- "end": 7.586,
61
- "text": "Hey, good afternoon everybody.",
62
- "visual": "Man in red shirt speaking to camera in medium shot. Home office with bookshelf. Natural lighting.",
63
- "words": [...]
64
- }
65
- ```
66
-
67
- **B-roll segments - insert new entries:**
68
- ```json
69
- {
70
- "start": 35.474,
71
- "end": 56.162,
72
- "text": "",
73
- "visual": "Green bicycle parked in front of building. Urban street with trees.",
74
- "b_roll": true,
75
- "words": []
76
- }
77
- ```
78
-
79
- **Guidelines:**
80
- - Descriptions should be 3 sentences max.
81
- - First segment: detailed (subject, setting, shot type, lighting, camera style)
82
- - Continuing shots: brief if similar, otherwise can be up to 3 sentences if drastically different.
83
-
84
- ### 5. Cleanup & Return
85
-
86
- ```bash
87
- rm -rf tmp/frames/[video_name]
88
- ```
89
-
90
- Return structured response:
91
- ```
92
- ✓ [video_filename.mov] analyzed successfully
93
- Visual transcript: <visual_transcript_path>
94
- Video path: <video_path>
95
- ```
96
-
97
- **DO NOT update library.yaml** - parent agent handles this to avoid race conditions in parallel execution.
@@ -1,25 +0,0 @@
1
- #!/usr/bin/env ruby
2
- require 'json'
3
-
4
- abort "Usage: ruby prepare_visual_script.rb <json_file>" if ARGV.empty?
5
- abort "Error: File not found: #{ARGV[0]}" unless File.exist?(ARGV[0])
6
-
7
- begin
8
- data = JSON.parse(File.read(ARGV[0]))
9
-
10
- data['segments']&.each { |s| s.delete('words') }
11
- data.delete('word_segments')
12
-
13
- # Reorder keys: language and video_path first, then segments, then everything else
14
- reordered = {}
15
- reordered['language'] = data['language'] if data['language']
16
- reordered['video_path'] = data['video_path'] if data['video_path']
17
- reordered['segments'] = data['segments'] if data['segments']
18
- # Add any other keys that might exist
19
- data.each { |k, v| reordered[k] = v unless reordered.key?(k) }
20
-
21
- File.write(ARGV[0], JSON.pretty_generate(reordered))
22
- puts "Prettified: #{ARGV[0]} (word-level timing removed)"
23
- rescue JSON::ParserError => e
24
- abort "Error: Invalid JSON - #{e.message}"
25
- end
@@ -1,26 +0,0 @@
1
- ---
2
- name: backup-library
3
- description: Creates compressed ZIP backups of libraries directory. Backs up library.yaml, transcripts, and roughcuts (not video files). This skill can also be useful when you need to restore a library.
4
- ---
5
-
6
- # Skill: Backup Library
7
-
8
- Verify libraries directory exists:
9
- ```bash
10
- ls -la libraries/
11
- ```
12
-
13
- Run backup:
14
- ```bash
15
- ruby .claude/skills/backup-library/backup_libraries.rb
16
- ```
17
-
18
- Creates `backups/libraries_YYYYMMDD_HHMMSS.zip` containing the entire libraries directory.
19
-
20
- ## Restore Library
21
-
22
- To restore from a backup, extract the ZIP file to the project root.
23
- ```bash
24
- unzip backups/libraries_timestamp.zip -d .
25
- ```
26
- This restores all libraries to their original locations.
@@ -1,46 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # Library Backup Utility
5
- # Creates compressed ZIP backups of the entire libraries directory
6
-
7
- require 'fileutils'
8
- require 'time'
9
- require 'zip'
10
-
11
- class LibraryBackup
12
- def initialize(project_root = Dir.pwd)
13
- @libraries_dir = File.join(project_root, 'libraries')
14
- @backups_dir = File.join(project_root, 'backups')
15
- end
16
-
17
- def backup
18
- unless Dir.exist?(@libraries_dir)
19
- puts "❌ No libraries directory found"
20
- return nil
21
- end
22
-
23
- FileUtils.mkdir_p(@backups_dir)
24
-
25
- timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
26
- backup_path = File.join(@backups_dir, "libraries_#{timestamp}.zip")
27
-
28
- puts "📦 Creating backup: #{backup_path}"
29
-
30
- files = Dir.glob(File.join(@libraries_dir, '**', '*')).select { |f| File.file?(f) }
31
-
32
- Zip::File.open(backup_path, create: true) do |zipfile|
33
- files.each do |file|
34
- zipfile.add(file.sub("#{File.dirname(@libraries_dir)}/", ''), file)
35
- end
36
- end
37
-
38
- puts "✅ Backed up #{files.size} files"
39
- backup_path
40
- end
41
- end
42
-
43
- # CLI
44
- if __FILE__ == $PROGRAM_NAME
45
- LibraryBackup.new.backup
46
- end
@@ -1,214 +0,0 @@
1
- ---
2
- name: release
3
- description: Creates a new ButterCut release with version bump, changelog, git tag, gem build, and GitHub release. Use when publishing a new version.
4
- ---
5
-
6
- # Skill: Release ButterCut
7
-
8
- Guides through the complete release process: version bump, changelog, git operations, gem publishing, and GitHub release creation.
9
-
10
- ## When to Use
11
-
12
- - Publishing a new version of ButterCut
13
- - After merging features or fixes that should be released
14
- - Creating the first v0.1.0 release
15
-
16
- ## Workflow
17
-
18
- ### 1. Run Tests First
19
-
20
- **CRITICAL: Always run tests before releasing. Never release if tests fail.**
21
-
22
- ```bash
23
- bundle exec rspec
24
- ```
25
-
26
- If any tests fail, STOP immediately and ask user to fix before proceeding with release.
27
-
28
- ### 2. Check Current State
29
-
30
- ```bash
31
- # Read current version
32
- cat lib/buttercut/version.rb
33
-
34
- # Check git status (must be clean)
35
- git status
36
-
37
- # Check existing tags
38
- git tag -l
39
- ```
40
-
41
- If git status is not clean, stop and ask user to commit or stash changes before proceeding.
42
-
43
- ### 3. Determine New Version
44
-
45
- Ask user what type of release following [Semantic Versioning](https://semver.org/):
46
- - **MAJOR** (1.0.0): Breaking changes
47
- - **MINOR** (0.2.0): New features, backward compatible
48
- - **PATCH** (0.1.1): Bug fixes, backward compatible
49
-
50
- Calculate new version number based on current version and release type.
51
-
52
- ### 4. Update Version File
53
-
54
- Edit `lib/buttercut/version.rb` with the new version:
55
-
56
- ```ruby
57
- class ButterCut
58
- VERSION = "0.2.0" # Update this
59
- end
60
- ```
61
-
62
- ### 5. Update Gemfile.lock
63
-
64
- Run `bundle install` so `Gemfile.lock` reflects the new version:
65
-
66
- ```bash
67
- bundle install
68
- ```
69
-
70
- Verify the version updated in `Gemfile.lock` before proceeding.
71
-
72
- ### 6. Gather Changelog Notes
73
-
74
- Ask user for release notes. Prompt with:
75
- - What changed in this release?
76
- - Any new features?
77
- - Any bug fixes?
78
- - Any breaking changes?
79
-
80
- ### 7. Update or Create CHANGELOG.md
81
-
82
- If `CHANGELOG.md` exists, prepend new entry. Otherwise create it:
83
-
84
- ```markdown
85
- # Changelog
86
-
87
- All notable changes to ButterCut will be documented in this file.
88
-
89
- ## [0.2.0] - 2025-01-21
90
-
91
- ### Added
92
- - Feature X
93
- - Support for Y
94
-
95
- ### Fixed
96
- - Bug in Z
97
-
98
- ### Changed
99
- - Improved W
100
- ```
101
-
102
- ### 8. Commit Version Bump
103
-
104
- ```bash
105
- git add lib/buttercut/version.rb Gemfile.lock CHANGELOG.md
106
- git commit -m "Bump version to 0.2.0"
107
- ```
108
-
109
- ### 9. Create and Push Git Tag
110
-
111
- ```bash
112
- git tag v0.2.0
113
- git push origin main
114
- git push origin v0.2.0
115
- ```
116
-
117
- ### 10. Build Gem
118
-
119
- ```bash
120
- gem build buttercut.gemspec
121
- ```
122
-
123
- This creates `buttercut-0.2.0.gem` file.
124
-
125
- ### 11. Publish to RubyGems
126
-
127
- **First time setup check:**
128
-
129
- If this is the first release, verify RubyGems authentication:
130
- ```bash
131
- gem signin
132
- ```
133
-
134
- If not authenticated, provide instructions:
135
- 1. Sign up at https://rubygems.org
136
- 2. Run `gem signin` and follow prompts
137
- 3. Store credentials for future releases
138
-
139
- **Publish the gem:**
140
- ```bash
141
- gem push buttercut-0.2.0.gem
142
- ```
143
-
144
- This makes the gem available for `gem install buttercut` worldwide.
145
-
146
- ### 12. Create GitHub Release
147
-
148
- **Using GitHub CLI:**
149
- ```bash
150
- gh release create v0.2.0 \
151
- --title "v0.2.0" \
152
- --notes "[Release notes from changelog]" \
153
- buttercut-0.2.0.gem
154
- ```
155
-
156
- **If `gh` CLI not available:**
157
-
158
- Guide user through manual release creation:
159
- 1. Go to https://github.com/andrewford/buttercut/releases/new
160
- 2. Choose tag: v0.2.0
161
- 3. Set title: v0.2.0
162
- 4. Paste changelog notes in description
163
- 5. Attach buttercut-0.2.0.gem file
164
- 6. Click "Publish release"
165
-
166
- Then wait for user confirmation that release is created before proceeding to cleanup.
167
-
168
- ### 13. Cleanup
169
-
170
- ```bash
171
- # Remove local gem file (it's on RubyGems and GitHub now)
172
- rm buttercut-0.2.0.gem
173
- ```
174
-
175
- ### 14. Verify Release
176
-
177
- Check that everything worked:
178
- - RubyGems page: https://rubygems.org/gems/buttercut
179
- - GitHub releases: https://github.com/andrewford/buttercut/releases
180
- - Git tags: `git tag -l`
181
-
182
- ### 15. Return Success Response
183
-
184
- Provide summary:
185
- ```
186
- ✓ ButterCut 0.2.0 released successfully
187
-
188
- Version: 0.2.0
189
- Git tag: v0.2.0
190
- RubyGems: Published at https://rubygems.org/gems/buttercut
191
- GitHub Release: https://github.com/andrewford/buttercut/releases/tag/v0.2.0
192
-
193
- Installation:
194
- gem install buttercut
195
-
196
- Upgrade:
197
- gem update buttercut
198
- ```
199
-
200
- ## Critical Principles
201
-
202
- **Always run tests first** - Never release if tests fail
203
- **Git must be clean** - No uncommitted changes before release
204
- **Push before publish** - Tags must be pushed before creating GitHub release
205
- **Semantic versioning** - Follow semver strictly for version numbers
206
- **Changelog required** - Every release needs documented changes
207
-
208
- ## Common Issues
209
-
210
- **Tests failing:** Ask user to fix tests before proceeding
211
- **Git not clean:** Ask user to commit or stash changes first
212
- **Tag already exists:** Verify this isn't a duplicate release
213
- **RubyGems authentication:** Guide through `gem signin` process
214
- **GitHub CLI not installed:** Provide manual release instructions
@@ -1,71 +0,0 @@
1
- ---
2
- name: roughcut
3
- description: Creates video rough cut yaml file for use with Buttercut gem. Concatenates visual transcripts with file markers, creates a roughcut yaml with clip selections, then exports to XML format. Use this skill when users want a "roughcut", "sequence" or "scene" generated. These are all the same thing, just with different lengths.
4
- ---
5
-
6
- # Skill: Create Rough Cut
7
-
8
- This skill handles the editorial process of creating rough cut timeline scripts from transcribed video footage. It launches a specialized agent that analyzes transcripts, makes editorial decisions, outputs a structured YAML rough cut, and exports it to Final Cut Pro XML format.
9
-
10
- **Note:** This skill is used for both full-length rough cuts (multiple minutes) and short sequences (30-60 seconds).
11
-
12
- ## Prerequisites Check
13
-
14
- Before launching the roughcut agent, verify all transcripts are complete:
15
-
16
- 1. **Check library exists:**
17
- ```bash
18
- ls libraries/[library-name]/library.yaml
19
- ```
20
-
21
- 2. **Verify visual transcripts:**
22
- Read `libraries/[library-name]/library.yaml` and check that every video entry has both:
23
- - `transcript` populated (audio transcript filename)
24
- - `visual_transcript` populated (visual descriptions filename)
25
-
26
- If any visual transcripts are missing:
27
- - Inform user that transcript processing must be completed first
28
- - Ask if they want Claude to finish transcript processing using the `transcribe-audio` and `analyze-video` skills
29
- - Do not proceed with roughcut creation until all transcripts are complete
30
-
31
- ## Launch Roughcut Agent
32
-
33
- Once prerequisites are verified, launch the roughcut creation agent using the Task tool:
34
-
35
- ```
36
- Task tool with:
37
- - subagent_type: "general-purpose"
38
- - description: "Create rough cut from visual transcripts"
39
- - prompt: [See agent prompt template below]
40
- ```
41
-
42
- ### Agent Prompt Template
43
-
44
- When launching the agent, provide a detailed prompt with all necessary context:
45
-
46
- ```
47
- You are a video editor AI agent creating a rough cut or sequence for the "{library_name}" library.
48
-
49
- USER REQUEST: {what_user_asked_for}
50
-
51
- LIBRARY CONTEXT:
52
- {paste relevant content from library.yaml - footage_summary, user_context, etc.}
53
-
54
- YOUR TASK:
55
- 1. Read the roughcut creation instructions from .claude/skills/roughcut/agent_instructions.md
56
- 2. Follow those instructions to create the rough cut
57
- 3. Return the paths to the created YAML and XML files when complete
58
-
59
- DELIVERABLES:
60
- - Rough cut YAML file at: libraries/{library_name}/roughcuts/{roughcut_name}_{datetime}.yaml
61
- - Exported XML file for user's chosen video editor
62
- - Backup created via backup-library skill
63
-
64
- Begin by reading the agent instructions file.
65
- ```
66
-
67
- ## After Agent Completes
68
-
69
- When the agent returns:
70
- 1. Inform the user of the created roughcut file (the xml file, not the yaml file) and its location
71
- 2. Confirm the rough cut is ready to import into their video editor