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.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.prettierignore +2 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +13 -0
  6. data/CHANGELOG.md +5 -0
  7. data/Gemfile +5 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +187 -0
  10. data/Rakefile +12 -0
  11. data/bagel.gemspec +32 -0
  12. data/bin/console +15 -0
  13. data/bin/setup +8 -0
  14. data/lib/bagel/colors.rb +8 -0
  15. data/lib/bagel/fonts.rb +3 -0
  16. data/lib/bagel/graphic/comment.rb +49 -0
  17. data/lib/bagel/graphic/intro.rb +49 -0
  18. data/lib/bagel/graphic/scoreboard/context.rb +28 -0
  19. data/lib/bagel/graphic/scoreboard/games.rb +13 -0
  20. data/lib/bagel/graphic/scoreboard/name.rb +32 -0
  21. data/lib/bagel/graphic/scoreboard/points.rb +13 -0
  22. data/lib/bagel/graphic/scoreboard/score.rb +38 -0
  23. data/lib/bagel/graphic/scoreboard/server.rb +16 -0
  24. data/lib/bagel/graphic/scoreboard/sets.rb +13 -0
  25. data/lib/bagel/graphic/scoreboard.rb +102 -0
  26. data/lib/bagel/graphic/stats/duration.rb +33 -0
  27. data/lib/bagel/graphic/stats/header.rb +32 -0
  28. data/lib/bagel/graphic/stats/result.rb +41 -0
  29. data/lib/bagel/graphic/stats/stat.rb +48 -0
  30. data/lib/bagel/graphic/stats.rb +80 -0
  31. data/lib/bagel/graphic.rb +23 -0
  32. data/lib/bagel/tennis/checker.rb +100 -0
  33. data/lib/bagel/tennis/outcome.rb +48 -0
  34. data/lib/bagel/tennis/player.rb +22 -0
  35. data/lib/bagel/tennis/point.rb +17 -0
  36. data/lib/bagel/tennis/score.rb +68 -0
  37. data/lib/bagel/tennis/scoring.rb +26 -0
  38. data/lib/bagel/tennis/serve.rb +36 -0
  39. data/lib/bagel/tennis/stats/aces.rb +28 -0
  40. data/lib/bagel/tennis/stats/break_points_won.rb +33 -0
  41. data/lib/bagel/tennis/stats/double_faults.rb +28 -0
  42. data/lib/bagel/tennis/stats/first_serve_in.rb +33 -0
  43. data/lib/bagel/tennis/stats/first_serve_points_won.rb +33 -0
  44. data/lib/bagel/tennis/stats/net_points_won.rb +33 -0
  45. data/lib/bagel/tennis/stats/points_won.rb +24 -0
  46. data/lib/bagel/tennis/stats/second_serve_points_won.rb +33 -0
  47. data/lib/bagel/tennis/stats/stat.rb +40 -0
  48. data/lib/bagel/tennis/stats/unforced_errors.rb +24 -0
  49. data/lib/bagel/tennis/stats/winners.rb +24 -0
  50. data/lib/bagel/tennis/stats.rb +16 -0
  51. data/lib/bagel/tennis.rb +13 -0
  52. data/lib/bagel/timeframe.rb +31 -0
  53. data/lib/bagel/version.rb +5 -0
  54. data/lib/bagel/video/clip.rb +41 -0
  55. data/lib/bagel/video/clip_builder_factory.rb +13 -0
  56. data/lib/bagel/video/clip_builders/clip_builder.rb +32 -0
  57. data/lib/bagel/video/clip_builders/intro_clip_builder.rb +37 -0
  58. data/lib/bagel/video/clip_builders/point_clip_builder.rb +67 -0
  59. data/lib/bagel/video/clip_builders/stats_clip_builder.rb +56 -0
  60. data/lib/bagel/video/clip_data_parser.rb +39 -0
  61. data/lib/bagel/video/clip_director.rb +16 -0
  62. data/lib/bagel/video/clip_saver.rb +59 -0
  63. data/lib/bagel/video/fade.rb +24 -0
  64. data/lib/bagel/video/ffmpeg/concat_file.rb +45 -0
  65. data/lib/bagel/video/ffmpeg/fade_filter.rb +34 -0
  66. data/lib/bagel/video/ffmpeg/overlay_filter.rb +52 -0
  67. data/lib/bagel/video/ffmpeg.rb +41 -0
  68. data/lib/bagel/video/orientation.rb +8 -0
  69. data/lib/bagel/video/overlays/comment.rb +15 -0
  70. data/lib/bagel/video/overlays/intro.rb +15 -0
  71. data/lib/bagel/video/overlays/overlay.rb +28 -0
  72. data/lib/bagel/video/overlays/scoreboard.rb +15 -0
  73. data/lib/bagel/video/overlays/second_serve.rb +15 -0
  74. data/lib/bagel/video/overlays/stats.rb +15 -0
  75. data/lib/bagel/video/padding.rb +9 -0
  76. data/lib/bagel/video/video.rb +18 -0
  77. data/lib/bagel/video.rb +32 -0
  78. data/lib/bagel.rb +76 -0
  79. 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