bagel 0.1.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 +7 -0
- data/.gitignore +13 -0
- data/.prettierignore +2 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +187 -0
- data/Rakefile +12 -0
- data/bagel.gemspec +32 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/bagel/colors.rb +8 -0
- data/lib/bagel/fonts.rb +3 -0
- data/lib/bagel/graphic/comment.rb +49 -0
- data/lib/bagel/graphic/intro.rb +49 -0
- data/lib/bagel/graphic/scoreboard/context.rb +28 -0
- data/lib/bagel/graphic/scoreboard/games.rb +13 -0
- data/lib/bagel/graphic/scoreboard/name.rb +32 -0
- data/lib/bagel/graphic/scoreboard/points.rb +13 -0
- data/lib/bagel/graphic/scoreboard/score.rb +38 -0
- data/lib/bagel/graphic/scoreboard/server.rb +16 -0
- data/lib/bagel/graphic/scoreboard/sets.rb +13 -0
- data/lib/bagel/graphic/scoreboard.rb +102 -0
- data/lib/bagel/graphic/stats/duration.rb +33 -0
- data/lib/bagel/graphic/stats/header.rb +32 -0
- data/lib/bagel/graphic/stats/result.rb +41 -0
- data/lib/bagel/graphic/stats/stat.rb +48 -0
- data/lib/bagel/graphic/stats.rb +80 -0
- data/lib/bagel/graphic.rb +23 -0
- data/lib/bagel/tennis/checker.rb +100 -0
- data/lib/bagel/tennis/outcome.rb +48 -0
- data/lib/bagel/tennis/player.rb +22 -0
- data/lib/bagel/tennis/point.rb +17 -0
- data/lib/bagel/tennis/score.rb +68 -0
- data/lib/bagel/tennis/scoring.rb +26 -0
- data/lib/bagel/tennis/serve.rb +36 -0
- data/lib/bagel/tennis/stats/aces.rb +28 -0
- data/lib/bagel/tennis/stats/break_points_won.rb +33 -0
- data/lib/bagel/tennis/stats/double_faults.rb +28 -0
- data/lib/bagel/tennis/stats/first_serve_in.rb +33 -0
- data/lib/bagel/tennis/stats/first_serve_points_won.rb +33 -0
- data/lib/bagel/tennis/stats/net_points_won.rb +33 -0
- data/lib/bagel/tennis/stats/points_won.rb +24 -0
- data/lib/bagel/tennis/stats/second_serve_points_won.rb +33 -0
- data/lib/bagel/tennis/stats/stat.rb +40 -0
- data/lib/bagel/tennis/stats/unforced_errors.rb +24 -0
- data/lib/bagel/tennis/stats/winners.rb +24 -0
- data/lib/bagel/tennis/stats.rb +16 -0
- data/lib/bagel/tennis.rb +13 -0
- data/lib/bagel/timeframe.rb +31 -0
- data/lib/bagel/version.rb +5 -0
- data/lib/bagel/video/clip.rb +41 -0
- data/lib/bagel/video/clip_builder_factory.rb +13 -0
- data/lib/bagel/video/clip_builders/clip_builder.rb +32 -0
- data/lib/bagel/video/clip_builders/intro_clip_builder.rb +37 -0
- data/lib/bagel/video/clip_builders/point_clip_builder.rb +67 -0
- data/lib/bagel/video/clip_builders/stats_clip_builder.rb +56 -0
- data/lib/bagel/video/clip_data_parser.rb +39 -0
- data/lib/bagel/video/clip_director.rb +16 -0
- data/lib/bagel/video/clip_saver.rb +59 -0
- data/lib/bagel/video/fade.rb +24 -0
- data/lib/bagel/video/ffmpeg/concat_file.rb +45 -0
- data/lib/bagel/video/ffmpeg/fade_filter.rb +34 -0
- data/lib/bagel/video/ffmpeg/overlay_filter.rb +52 -0
- data/lib/bagel/video/ffmpeg.rb +41 -0
- data/lib/bagel/video/orientation.rb +8 -0
- data/lib/bagel/video/overlays/comment.rb +15 -0
- data/lib/bagel/video/overlays/intro.rb +15 -0
- data/lib/bagel/video/overlays/overlay.rb +28 -0
- data/lib/bagel/video/overlays/scoreboard.rb +15 -0
- data/lib/bagel/video/overlays/second_serve.rb +15 -0
- data/lib/bagel/video/overlays/stats.rb +15 -0
- data/lib/bagel/video/padding.rb +9 -0
- data/lib/bagel/video/video.rb +18 -0
- data/lib/bagel/video.rb +32 -0
- data/lib/bagel.rb +76 -0
- metadata +196 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
module Bagel::Video::ClipBuilders
|
2
|
+
class PointClipBuilder < ClipBuilder
|
3
|
+
private attr_reader :names, :score, :second_serve, :comment
|
4
|
+
|
5
|
+
ID_SUFFIX = '-point'
|
6
|
+
COMMENT_2ND_SERVE = '2nd SERVE'
|
7
|
+
|
8
|
+
def set_id
|
9
|
+
clip.id = "#{score.id}#{ID_SUFFIX}"
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_clip_fade
|
13
|
+
# no fade for point clips
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_overlays
|
17
|
+
add_scoreboard_overlay
|
18
|
+
add_second_serve_overlay if second_serve
|
19
|
+
add_comment_overlay if comment
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def post_initialize(data)
|
25
|
+
@names = Bagel.config.names
|
26
|
+
@score = Score.new(sets: data[:score].first, points: data[:score].last, server: data[:server])
|
27
|
+
@second_serve = data[:faults] > 0
|
28
|
+
@comment = data[:comment]
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_scoreboard_overlay
|
32
|
+
clip.overlays << scoreboard_overlay
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_second_serve_overlay
|
36
|
+
clip.overlays << second_serve_overlay
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_comment_overlay
|
40
|
+
clip.overlays << comment_overlay
|
41
|
+
end
|
42
|
+
|
43
|
+
def comment_overlay
|
44
|
+
Overlays::Comment.new(comment_graphic)
|
45
|
+
end
|
46
|
+
|
47
|
+
def scoreboard_overlay
|
48
|
+
Overlays::Scoreboard.new(scoreboard_graphic)
|
49
|
+
end
|
50
|
+
|
51
|
+
def second_serve_overlay
|
52
|
+
Overlays::SecondServe.new(second_serve_graphic)
|
53
|
+
end
|
54
|
+
|
55
|
+
def scoreboard_graphic
|
56
|
+
Graphic::Scoreboard.new(score, names).save
|
57
|
+
end
|
58
|
+
|
59
|
+
def second_serve_graphic
|
60
|
+
@second_serve_graphic ||= Graphic::Comment.new(COMMENT_2ND_SERVE).save
|
61
|
+
end
|
62
|
+
|
63
|
+
def comment_graphic
|
64
|
+
Graphic::Comment.new(comment.upcase).save
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Bagel::Video::ClipBuilders
|
2
|
+
class StatsClipBuilder < ClipBuilder
|
3
|
+
private attr_reader :names, :score, :points
|
4
|
+
|
5
|
+
ID_SUFFIX = '-stats'
|
6
|
+
|
7
|
+
def set_id
|
8
|
+
clip.id = "#{score.set_number.to_s.rjust(2, '0')}#{ID_SUFFIX}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def add_clip_fade
|
12
|
+
clip.fade = Fade.new(out_start: 10.5, out_duration: 1.5)
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_overlays
|
16
|
+
add_scoreboard_overlay
|
17
|
+
add_stats_overlay
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def post_initialize(data)
|
23
|
+
@names = Bagel.config.names
|
24
|
+
@score = Score.new(sets: data[:score].first)
|
25
|
+
@points = ClipDataParser.new(Bagel.config.clip_data).parse_points(score.set_number)
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_scoreboard_overlay
|
29
|
+
clip.overlays << scoreboard_overlay
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_stats_overlay
|
33
|
+
clip.overlays << stats_overlay
|
34
|
+
end
|
35
|
+
|
36
|
+
def scoreboard_overlay
|
37
|
+
Overlays::Scoreboard.new(scoreboard_graphic, true)
|
38
|
+
end
|
39
|
+
|
40
|
+
def stats_overlay
|
41
|
+
Overlays::Stats.new(stats_graphic)
|
42
|
+
end
|
43
|
+
|
44
|
+
def scoreboard_graphic
|
45
|
+
Graphic::Scoreboard.new(score, names).save
|
46
|
+
end
|
47
|
+
|
48
|
+
def stats_graphic
|
49
|
+
Graphic::Stats.new(
|
50
|
+
names: Bagel.config.names,
|
51
|
+
score: score,
|
52
|
+
points: points
|
53
|
+
).save
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Bagel::Video
|
2
|
+
class ClipDataParser
|
3
|
+
private attr_reader :file
|
4
|
+
|
5
|
+
def initialize(file)
|
6
|
+
@file = file
|
7
|
+
end
|
8
|
+
|
9
|
+
def parse_clips
|
10
|
+
read_yaml.map { |data| parse_clip(data) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse_points(set_number)
|
14
|
+
read_yaml
|
15
|
+
.select { |data| data[:type] == 'point' }
|
16
|
+
.select { |data| data[:score].first.length == set_number }
|
17
|
+
.map do |data|
|
18
|
+
Point.new(
|
19
|
+
timeframe: Timeframe.new(data[:start], data[:finish]),
|
20
|
+
score: Score.new(sets: data[:score].first, points: data[:score].last, server: data[:server]),
|
21
|
+
serve: Serve.new(server: data[:server], faults: data[:faults]),
|
22
|
+
outcome: Outcome.new(winner: data[:winner], reason: data[:reason], shot: data[:shot], net: data[:net])
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def read_yaml
|
30
|
+
YAML.safe_load_file(file, symbolize_names: true)
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse_clip(data)
|
34
|
+
builder = ClipBuilderFactory.for(data[:type].to_sym, data)
|
35
|
+
director = ClipDirector.new(builder)
|
36
|
+
director.build_clip
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Bagel::Video
|
2
|
+
class ClipDirector
|
3
|
+
private attr_reader :builder
|
4
|
+
|
5
|
+
def initialize(builder)
|
6
|
+
@builder = builder
|
7
|
+
end
|
8
|
+
|
9
|
+
def build_clip
|
10
|
+
builder.set_id
|
11
|
+
builder.add_clip_fade
|
12
|
+
builder.add_overlays
|
13
|
+
builder.clip
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Bagel::Video
|
2
|
+
class ClipSaver
|
3
|
+
private attr_reader :clip
|
4
|
+
|
5
|
+
def initialize(clip)
|
6
|
+
@clip = clip
|
7
|
+
end
|
8
|
+
|
9
|
+
def save
|
10
|
+
FFMPEG.trim(
|
11
|
+
source: Bagel.config.source_video,
|
12
|
+
start: clip.start,
|
13
|
+
duration: clip.duration,
|
14
|
+
destination: clip.trim_path
|
15
|
+
)
|
16
|
+
|
17
|
+
FFMPEG.filter(
|
18
|
+
inputs: inputs,
|
19
|
+
filters: filters,
|
20
|
+
destination: clip.clip_path
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def inputs
|
27
|
+
[ clip.trim_path ] + clip.overlays.map(&:asset)
|
28
|
+
end
|
29
|
+
|
30
|
+
def filters
|
31
|
+
fade_filter_clip + fade_filters_overlay + overlay_filters
|
32
|
+
end
|
33
|
+
|
34
|
+
def fade_filter_clip
|
35
|
+
clip.fade ? [ FFMPEG::FadeFilter.for_clip(clip.fade) ] : []
|
36
|
+
end
|
37
|
+
|
38
|
+
def fade_filters_overlay
|
39
|
+
clip.overlays.map.with_index do |overlay, index|
|
40
|
+
next unless overlay.fade
|
41
|
+
FFMPEG::FadeFilter.for_overlay(overlay.fade, index)
|
42
|
+
end.compact
|
43
|
+
end
|
44
|
+
|
45
|
+
def overlay_filters
|
46
|
+
clip.overlays.map.with_index do |overlay, index|
|
47
|
+
is_last = index == clip.overlays.count - 1
|
48
|
+
|
49
|
+
FFMPEG::OverlayFilter.new(
|
50
|
+
fade: overlay.fade,
|
51
|
+
padding: overlay.padding,
|
52
|
+
orientation: overlay.orientation,
|
53
|
+
index: index,
|
54
|
+
is_last: is_last
|
55
|
+
).to_s
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Bagel::Video
|
2
|
+
class Fade
|
3
|
+
attr_reader :in_start, :in_duration, :out_start, :out_duration
|
4
|
+
|
5
|
+
def self.for_final_score
|
6
|
+
new(out_start: 3, out_duration: 1.5)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.for_stats_overlay
|
10
|
+
new(in_start: 3, in_duration: 1.5, out_start: 8, out_duration: 1.5)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.for_stats_clip
|
14
|
+
new(out_start: 10.5, out_duration: 1.5)
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(attrs)
|
18
|
+
@in_start = attrs[:in_start]
|
19
|
+
@in_duration = attrs[:in_duration]
|
20
|
+
@out_start = attrs[:out_start]
|
21
|
+
@out_duration = attrs[:out_duration]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Bagel::Video::FFMPEG
|
2
|
+
class ConcatFile
|
3
|
+
MP4_PATTERN = '*.mp4'
|
4
|
+
FFMPEG_FILE_DIRECTIVE_PREFIX = 'file' + ' '
|
5
|
+
|
6
|
+
attr_reader :source
|
7
|
+
|
8
|
+
def self.create(source, &block)
|
9
|
+
new(source).create(&block)
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(source)
|
13
|
+
@source = source
|
14
|
+
end
|
15
|
+
|
16
|
+
def create
|
17
|
+
Tempfile.create do |file|
|
18
|
+
file.write(contents)
|
19
|
+
file.flush
|
20
|
+
yield(file.path)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def contents
|
27
|
+
Dir.glob(MP4_PATTERN, base: source)
|
28
|
+
.sort
|
29
|
+
.map { |filename| ffmpeg_file_directive(filename) }
|
30
|
+
.join("\n")
|
31
|
+
end
|
32
|
+
|
33
|
+
def ffmpeg_file_directive(filename)
|
34
|
+
FFMPEG_FILE_DIRECTIVE_PREFIX + absolute_path(relative_path(filename))
|
35
|
+
end
|
36
|
+
|
37
|
+
def relative_path(filename)
|
38
|
+
File.join(source, filename)
|
39
|
+
end
|
40
|
+
|
41
|
+
def absolute_path(path)
|
42
|
+
File.expand_path(path)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Bagel::Video::FFMPEG
|
2
|
+
class FadeFilter
|
3
|
+
private attr_reader :fade, :input_index, :label, :alpha
|
4
|
+
|
5
|
+
def self.for_clip(fade)
|
6
|
+
new(INDEX_VIDEO, fade, LABEL_VIDEO, false).to_s
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.for_overlay(fade, input_index)
|
10
|
+
new(input_index+1, fade, "#{LABEL_OVERLAY}#{input_index}", true).to_s
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(input_index, fade, label, alpha=false)
|
14
|
+
@input_index = input_index
|
15
|
+
@fade = fade
|
16
|
+
@label = label
|
17
|
+
@alpha = alpha
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
chain = []
|
22
|
+
|
23
|
+
if fade.in_start && fade.in_duration
|
24
|
+
chain << "fade=t=in:st=#{fade.in_start}:d=#{fade.in_duration}:alpha=#{alpha ? '1' : '0'}"
|
25
|
+
end
|
26
|
+
|
27
|
+
if fade.out_start && fade.out_duration
|
28
|
+
chain << "fade=t=out:st=#{fade.out_start}:d=#{fade.out_duration}:alpha=#{alpha ? '1' : '0'}"
|
29
|
+
end
|
30
|
+
|
31
|
+
"[#{input_index}]#{chain.join(',')}[#{label}]"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Bagel::Video::FFMPEG
|
2
|
+
class OverlayFilter
|
3
|
+
private attr_reader :fade, :padding, :orientation, :index, :is_last
|
4
|
+
|
5
|
+
def initialize(fade:, padding:, orientation:, index:, is_last:)
|
6
|
+
@fade = fade
|
7
|
+
@padding = padding
|
8
|
+
@orientation = orientation
|
9
|
+
@index = index
|
10
|
+
@is_last = is_last
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
bottom = LABEL_VIDEO
|
15
|
+
bottom += (1..index).map { |n| LABEL_OVERLAY+n.to_s }.join if index > 0
|
16
|
+
bottom = "[#{bottom}]"
|
17
|
+
|
18
|
+
top = fade ? "[#{LABEL_OVERLAY}#{index}]" : "[#{index+1}]"
|
19
|
+
|
20
|
+
label_out = "[#{LABEL_VIDEO + (1..index+1).map { |n| LABEL_OVERLAY+n.to_s }.join}]"
|
21
|
+
label_out = '' if is_last
|
22
|
+
|
23
|
+
"#{bottom}#{top}overlay=x=(#{position_x}):y=(#{position_y}):shortest=1#{label_out}"
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def padding_x
|
29
|
+
padding ? padding.x : 0
|
30
|
+
end
|
31
|
+
|
32
|
+
def padding_y
|
33
|
+
padding ? padding.y : 0
|
34
|
+
end
|
35
|
+
|
36
|
+
def position_x
|
37
|
+
case orientation
|
38
|
+
when :top_left, :bottom_left then padding_x
|
39
|
+
when :top_right, :bottom_right then "main_w-overlay_w-#{padding_x}"
|
40
|
+
when :center, :bottom then "(main_w-overlay_w)/2"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def position_y
|
45
|
+
case orientation
|
46
|
+
when :top_left, :top_right then padding_y
|
47
|
+
when :bottom_left, :bottom_right, :bottom then "main_h-overlay_h-#{padding_y}"
|
48
|
+
when :center then "(main_h-overlay_h)/2"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'bagel/video/ffmpeg/concat_file'
|
2
|
+
|
3
|
+
module Bagel::Video
|
4
|
+
module FFMPEG
|
5
|
+
INDEX_VIDEO = 0
|
6
|
+
LABEL_VIDEO = 'v'
|
7
|
+
LABEL_OVERLAY = 'o'
|
8
|
+
|
9
|
+
def self.concat(source:, destination:, transcode: false)
|
10
|
+
ConcatFile.create(source) do |path|
|
11
|
+
command = "ffmpeg -y -f concat -safe 0"
|
12
|
+
command << " -i #{path}"
|
13
|
+
command << " -c copy" unless transcode
|
14
|
+
command << " #{destination}"
|
15
|
+
system(command)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.trim(start:, source:, duration:, transcode: false, destination:)
|
20
|
+
command = "ffmpeg -y"
|
21
|
+
command << " -ss #{start}"
|
22
|
+
command << " -i #{source}"
|
23
|
+
command << " -to #{duration}"
|
24
|
+
command << " -c copy" unless transcode
|
25
|
+
command << " -avoid_negative_ts make_zero #{destination}"
|
26
|
+
system(command)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.filter(inputs:, filters:, destination:)
|
30
|
+
inputs = inputs.map { |input| "-i #{input} -loop 1" }.join(' ')
|
31
|
+
filters = filters.join(';')
|
32
|
+
|
33
|
+
command = "ffmpeg -y"
|
34
|
+
command << " #{inputs}"
|
35
|
+
command << " -filter_complex \"#{filters}\""
|
36
|
+
command << " -c:a copy"
|
37
|
+
command << " #{destination}"
|
38
|
+
system(command)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|