speedrun 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/lib/speedrun/cli.rb +2 -1
- data/lib/speedrun/ffmpeg.rb +57 -7
- data/lib/speedrun/formatter.rb +5 -0
- data/lib/speedrun/progress.rb +17 -0
- data/lib/speedrun/trimmer.rb +39 -22
- data/lib/speedrun/version.rb +1 -1
- data/lib/speedrun.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d3fd70626ab0029a0a22690b09bb98c59860e679b807b201fbc40ff34aaebbde
|
|
4
|
+
data.tar.gz: bb29f4d74a711e2a32d928daf2f50bd3040e4949f045c3889054e7cf511ad0f2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b39704ccbd0c2a8cecf836d923bf52dd251568eb2ca7cc5801d77e9358290e96de6799d37c33040b74b85a715cab042ac853770060b82ac6095850c16997146c
|
|
7
|
+
data.tar.gz: 6fee192f0e6d3fb312f8f0e054e4ce2602a2e141f22e025ea27d4a9b5bc121e465abbc0b67bd21761c38ae3a8fb0f2aa5b9dd30a52bc065b51c426857bb16eb4
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.0] - 2025-11-03
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Streaming progress tracking for FFmpeg operations
|
|
12
|
+
- Progress bar support during video analysis and processing
|
|
13
|
+
- Quiet mode for less verbose output
|
|
14
|
+
- Enhanced time parsing and formatting utilities
|
|
15
|
+
- Comprehensive test coverage for progress tracking functionality
|
|
16
|
+
|
|
17
|
+
### Improved
|
|
18
|
+
- Real-time progress updates during video processing
|
|
19
|
+
- More robust time parsing and conversion
|
|
20
|
+
- Ability to suppress output with quiet flag
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- Added .claude directory to .gitignore for user-specific configuration protection
|
|
24
|
+
|
|
8
25
|
## [0.1.0] - 2025-11-03
|
|
9
26
|
|
|
10
27
|
### Added
|
data/lib/speedrun/cli.rb
CHANGED
data/lib/speedrun/ffmpeg.rb
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
require 'shellwords'
|
|
4
4
|
require 'tempfile'
|
|
5
|
+
require 'open3'
|
|
5
6
|
|
|
6
7
|
module Speedrun
|
|
7
8
|
module FFmpeg
|
|
8
9
|
FREEZE_START_PATTERN = /freeze_start:\s*([\d.]+)/
|
|
9
10
|
FREEZE_END_PATTERN = /freeze_end:\s*([\d.]+)/
|
|
11
|
+
DURATION_PATTERN = /Duration:\s*(\d{2}):(\d{2}):(\d{2}\.\d+)/
|
|
12
|
+
TIME_PATTERN = /time=(\d{2}):(\d{2}):(\d{2}\.\d+)/
|
|
10
13
|
|
|
11
14
|
def self.parse_duration(output)
|
|
12
15
|
output.strip.to_f
|
|
@@ -44,7 +47,7 @@ module Speedrun
|
|
|
44
47
|
parse_duration(output)
|
|
45
48
|
end
|
|
46
49
|
|
|
47
|
-
def self.detect_freezes(file_path, noise_threshold: -70, min_duration: 1.0)
|
|
50
|
+
def self.detect_freezes(file_path, noise_threshold: -70, min_duration: 1.0, quiet: false, &block)
|
|
48
51
|
raise ArgumentError, "File not found: #{file_path}" unless File.exist?(file_path)
|
|
49
52
|
|
|
50
53
|
cmd = [
|
|
@@ -53,13 +56,37 @@ module Speedrun
|
|
|
53
56
|
'-vf', "freezedetect=n=#{noise_threshold}dB:d=#{min_duration}",
|
|
54
57
|
'-f', 'null',
|
|
55
58
|
'-'
|
|
56
|
-
]
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
output = String.new
|
|
62
|
+
duration = nil
|
|
63
|
+
|
|
64
|
+
Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thr|
|
|
65
|
+
stdin.close
|
|
66
|
+
|
|
67
|
+
stderr.each_line do |line|
|
|
68
|
+
output << line
|
|
69
|
+
|
|
70
|
+
# Extract duration from first line that contains it
|
|
71
|
+
if duration.nil? && line =~ DURATION_PATTERN
|
|
72
|
+
duration = Formatter.parse_time("#{$1}:#{$2}:#{$3}")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Extract current time and calculate progress
|
|
76
|
+
if duration && block_given? && line =~ TIME_PATTERN
|
|
77
|
+
current_time = Formatter.parse_time("#{$1}:#{$2}:#{$3}")
|
|
78
|
+
progress = [(current_time / duration * 100).round, 100].min
|
|
79
|
+
block.call(progress)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
wait_thr.value # Wait for process to complete
|
|
84
|
+
end
|
|
57
85
|
|
|
58
|
-
output = `#{cmd} 2>&1`
|
|
59
86
|
parse_freezes(output)
|
|
60
87
|
end
|
|
61
88
|
|
|
62
|
-
def self.extract_and_concat(input_file, output_file, keep_regions)
|
|
89
|
+
def self.extract_and_concat(input_file, output_file, keep_regions, quiet: false, &block)
|
|
63
90
|
raise ArgumentError, "File not found: #{input_file}" unless File.exist?(input_file)
|
|
64
91
|
raise ArgumentError, "No regions to keep" if keep_regions.empty?
|
|
65
92
|
|
|
@@ -81,10 +108,33 @@ module Speedrun
|
|
|
81
108
|
'-c', 'copy',
|
|
82
109
|
'-y',
|
|
83
110
|
output_file
|
|
84
|
-
]
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
output = String.new
|
|
114
|
+
duration = nil
|
|
115
|
+
success = false
|
|
116
|
+
|
|
117
|
+
Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thr|
|
|
118
|
+
stdin.close
|
|
119
|
+
|
|
120
|
+
stderr.each_line do |line|
|
|
121
|
+
output << line
|
|
85
122
|
|
|
86
|
-
|
|
87
|
-
|
|
123
|
+
# Extract duration from first line that contains it
|
|
124
|
+
if duration.nil? && line =~ DURATION_PATTERN
|
|
125
|
+
duration = Formatter.parse_time("#{$1}:#{$2}:#{$3}")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Extract current time and calculate progress
|
|
129
|
+
if duration && block_given? && line =~ TIME_PATTERN
|
|
130
|
+
current_time = Formatter.parse_time("#{$1}:#{$2}:#{$3}")
|
|
131
|
+
progress = [(current_time / duration * 100).round, 100].min
|
|
132
|
+
block.call(progress)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
success = wait_thr.value.success?
|
|
137
|
+
end
|
|
88
138
|
|
|
89
139
|
raise "FFmpeg failed: #{output}" unless success
|
|
90
140
|
|
data/lib/speedrun/formatter.rb
CHANGED
|
@@ -16,6 +16,11 @@ module Speedrun
|
|
|
16
16
|
format('%02d:%02d:%06.3f', hours, minutes, secs)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
def self.parse_time(time_string)
|
|
20
|
+
hours, minutes, seconds = time_string.split(':').map(&:to_f)
|
|
21
|
+
(hours * SECONDS_PER_HOUR) + (minutes * SECONDS_PER_MINUTE) + seconds
|
|
22
|
+
end
|
|
23
|
+
|
|
19
24
|
def self.format_duration(seconds)
|
|
20
25
|
if seconds < SECONDS_PER_MINUTE
|
|
21
26
|
format('%.2fs', seconds)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby-progressbar"
|
|
4
|
+
|
|
5
|
+
module Speedrun
|
|
6
|
+
module Progress
|
|
7
|
+
def self.create(title:, total:, quiet: false)
|
|
8
|
+
return nil if quiet
|
|
9
|
+
|
|
10
|
+
ProgressBar.create(
|
|
11
|
+
title: title,
|
|
12
|
+
total: total,
|
|
13
|
+
format: "%t: |%B| %p%%"
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/speedrun/trimmer.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Speedrun
|
|
|
7
7
|
attr_reader :input_file, :output_file
|
|
8
8
|
attr_accessor :dry_run
|
|
9
9
|
|
|
10
|
-
def initialize(input_file, output_file = nil, noise_threshold: -70, min_duration: 1.0)
|
|
10
|
+
def initialize(input_file, output_file = nil, noise_threshold: -70, min_duration: 1.0, quiet: false)
|
|
11
11
|
raise ArgumentError, "File not found: #{input_file}" unless File.exist?(input_file)
|
|
12
12
|
|
|
13
13
|
@input_file = input_file
|
|
@@ -15,26 +15,36 @@ module Speedrun
|
|
|
15
15
|
@noise_threshold = noise_threshold
|
|
16
16
|
@min_duration = min_duration
|
|
17
17
|
@dry_run = false
|
|
18
|
+
@quiet = quiet
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
def dry_run?
|
|
21
22
|
@dry_run
|
|
22
23
|
end
|
|
23
24
|
|
|
25
|
+
def quiet?
|
|
26
|
+
@quiet
|
|
27
|
+
end
|
|
28
|
+
|
|
24
29
|
def run
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
output "Input: #{@input_file}"
|
|
31
|
+
output "Output: #{@output_file}"
|
|
32
|
+
output ""
|
|
28
33
|
|
|
29
34
|
# Step 1: Detect freeze regions
|
|
35
|
+
progress_bar = Progress.create(title: "Analyzing video", total: 100, quiet: @quiet)
|
|
30
36
|
freeze_regions = FFmpeg.detect_freezes(
|
|
31
37
|
@input_file,
|
|
32
38
|
noise_threshold: @noise_threshold,
|
|
33
|
-
min_duration: @min_duration
|
|
34
|
-
|
|
39
|
+
min_duration: @min_duration,
|
|
40
|
+
quiet: @quiet
|
|
41
|
+
) do |progress|
|
|
42
|
+
progress_bar&.progress = progress if progress
|
|
43
|
+
end
|
|
44
|
+
progress_bar&.finish
|
|
35
45
|
|
|
36
46
|
if freeze_regions.empty?
|
|
37
|
-
|
|
47
|
+
output "No freeze regions detected. Copying original video..."
|
|
38
48
|
FileUtils.cp(@input_file, @output_file) unless dry_run?
|
|
39
49
|
return
|
|
40
50
|
end
|
|
@@ -52,21 +62,28 @@ module Speedrun
|
|
|
52
62
|
|
|
53
63
|
# Step 4: Process or dry-run
|
|
54
64
|
if dry_run?
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
output ""
|
|
66
|
+
output "DRY RUN - No files were modified"
|
|
57
67
|
else
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
FFmpeg.extract_and_concat(@input_file, @output_file, keep_regions)
|
|
68
|
+
output ""
|
|
69
|
+
progress_bar = Progress.create(title: "Processing video", total: 100, quiet: @quiet)
|
|
70
|
+
FFmpeg.extract_and_concat(@input_file, @output_file, keep_regions, quiet: @quiet) do |progress|
|
|
71
|
+
progress_bar&.progress = progress if progress
|
|
72
|
+
end
|
|
73
|
+
progress_bar&.finish
|
|
61
74
|
|
|
62
75
|
output_size = File.size(@output_file)
|
|
63
|
-
|
|
64
|
-
|
|
76
|
+
output "Complete! Output saved to #{@output_file}"
|
|
77
|
+
output " File size: #{Formatter.format_filesize(output_size)}"
|
|
65
78
|
end
|
|
66
79
|
end
|
|
67
80
|
|
|
68
81
|
private
|
|
69
82
|
|
|
83
|
+
def output(message)
|
|
84
|
+
puts message unless @quiet
|
|
85
|
+
end
|
|
86
|
+
|
|
70
87
|
def generate_output_filename(input_file)
|
|
71
88
|
ext = File.extname(input_file)
|
|
72
89
|
base = File.basename(input_file, ext)
|
|
@@ -98,14 +115,14 @@ module Speedrun
|
|
|
98
115
|
total_removed = freeze_regions.sum { |s, e| e - s }
|
|
99
116
|
total_kept = keep_regions.sum { |s, e| e - s }
|
|
100
117
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
118
|
+
output "Analysis complete:"
|
|
119
|
+
output " Found #{freeze_regions.length} freeze region#{freeze_regions.length == 1 ? '' : 's'}"
|
|
120
|
+
output " Keeping #{keep_regions.length} segment#{keep_regions.length == 1 ? '' : 's'} with motion"
|
|
121
|
+
output ""
|
|
122
|
+
output "Summary:"
|
|
123
|
+
output " Original duration: #{Formatter.format_duration(video_duration)}"
|
|
124
|
+
output " Keeping: #{Formatter.format_duration(total_kept)} (#{'%.1f' % (total_kept / video_duration * 100)}%)"
|
|
125
|
+
output " Removing: #{Formatter.format_duration(total_removed)} (#{'%.1f' % (total_removed / video_duration * 100)}%)"
|
|
109
126
|
end
|
|
110
127
|
end
|
|
111
128
|
end
|
data/lib/speedrun/version.rb
CHANGED
data/lib/speedrun.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: speedrun
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Benjamin Jackson
|
|
@@ -112,6 +112,7 @@ files:
|
|
|
112
112
|
- lib/speedrun/cli.rb
|
|
113
113
|
- lib/speedrun/ffmpeg.rb
|
|
114
114
|
- lib/speedrun/formatter.rb
|
|
115
|
+
- lib/speedrun/progress.rb
|
|
115
116
|
- lib/speedrun/trimmer.rb
|
|
116
117
|
- lib/speedrun/version.rb
|
|
117
118
|
- sig/speedrun.rbs
|