appydave-tools 0.11.10 → 0.12.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e7b8ce2de26afecd3f37579dc6b1a414de7f28812ad4399a01f3d3adf2e23b8
4
- data.tar.gz: 3156df843020c573448b8360f498b7c82510fba18a226339d07328f1cca6057c
3
+ metadata.gz: e50556323b419ef5468e5199cf148812e25ff5c6001db7d35dc7f4cd52d7bb50
4
+ data.tar.gz: 23015d1c15d2fcde38f973fed02add0b7f55d09be3acda11f06705a15fcd6fdf
5
5
  SHA512:
6
- metadata.gz: 15790b46559390d9a4e0816bc78c30d8204032daa156be756679762e27e48b4999b4ca5e35b23144dd986a6ce98ccd4bbc6ae1352a20c818527582e9b9a72475
7
- data.tar.gz: 5afd2d9429afa607c4f79134a4dceee6f1afb0b852bc4125ecfa2b5cd2fb084402d1c64ad79d5e39237db829dab424446bfb1ea3fb4e4df993dc71725295a922
6
+ metadata.gz: 9f097fe81dae81d638bfa6040b44f9da187c95892a903a2e2c9e91c493134fd18fe0541e0b77daa99bfc56fa84378a1527d6d43f0955d74fdc038f3e9c4271a4
7
+ data.tar.gz: bdbf1c0ec19ce47042e17a16b8af96110e0e9b037fd61336debb32a5d251d80e50ce2259d4130b9bc220e9d95404b70c34b9ae9340e8ec557a1a595e0614e388
data/.rubocop.yml CHANGED
@@ -91,6 +91,7 @@ Metrics/ParameterLists:
91
91
  Exclude:
92
92
  - "**/spec/**/*"
93
93
  - "**/models/**/*"
94
+ - "lib/appydave/tools/subtitle_master/join.rb"
94
95
  Layout/EmptyLineBetweenDefs:
95
96
  Exclude:
96
97
  - "**/spec/**/*"
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [0.11.11](https://github.com/appydave/appydave-tools/compare/v0.11.10...v0.11.11) (2024-11-26)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * update clean srt specs ([e0da656](https://github.com/appydave/appydave-tools/commit/e0da656a3bf04f094bc3726a588a59d99d3d24a8))
7
+
8
+ ## [0.11.10](https://github.com/appydave/appydave-tools/compare/v0.11.9...v0.11.10) (2024-11-26)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * Add future requirement docs for SRT ([29eb02d](https://github.com/appydave/appydave-tools/commit/29eb02de587c3e61f6b92a1690dc5a3c657c0a34))
14
+
1
15
  ## [0.11.9](https://github.com/appydave/appydave-tools/compare/v0.11.8...v0.11.9) (2024-11-26)
2
16
 
3
17
 
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # CHAT: https://chatgpt.com/c/67038d52-9928-8002-8063-5616f7fe7aef
5
+
6
+ # !/usr/bin/env ruby
7
+
8
+ require 'fileutils'
9
+ require 'optparse'
10
+
11
+ # Default base directory
12
+ base_dir = '/Volumes/Expansion/Sync/tube-channels/video-projects'
13
+
14
+ # Common subfolder names (for reference, not enforced)
15
+ # Common subfolders: intro, outro, content, teaser, thumb
16
+
17
+ # Parse command-line options
18
+ options = {}
19
+ OptionParser.new do |opts|
20
+ opts.banner = "Usage: #{$PROGRAM_NAME} -f <folder> <section> <prefix>"
21
+
22
+ opts.on('-f FOLDER', '--folder FOLDER', 'Specify the subfolder under video-projects') do |folder|
23
+ options[:folder] = folder
24
+ end
25
+ end.parse!
26
+
27
+ # Ensure the folder option is provided
28
+ unless options[:folder]
29
+ puts 'You must specify a folder using the -f option'
30
+ exit 1
31
+ end
32
+
33
+ # Set source and destination directories
34
+ source_dir = File.expand_path('~/Sync/smart-downloads/download-images')
35
+ dest_dir = File.join(base_dir, options[:folder], 'assets')
36
+
37
+ # Get input parameters
38
+ section = ARGV[0]
39
+ prefix = ARGV[1]
40
+
41
+ unless prefix && section
42
+ puts "Usage: #{$PROGRAM_NAME} -f <folder> <section> <prefix>"
43
+ exit 1
44
+ end
45
+
46
+ # Ensure the section subfolder exists
47
+ section_dir = File.join(dest_dir, section)
48
+ puts "Creating subfolder if it doesn't exist: #{section_dir}"
49
+ FileUtils.mkdir_p(section_dir)
50
+
51
+ puts "Source directory: #{source_dir}"
52
+
53
+ # Find and move the images
54
+ Dir.glob("#{source_dir}/*.jpg").each_with_index do |file, index|
55
+ puts "Processing #{file}, #{index}"
56
+ new_filename = "#{prefix}-#{section}-#{index + 1}.jpg"
57
+ puts "New filename: #{new_filename}"
58
+ destination = File.join(section_dir, new_filename)
59
+ puts "Destination: #{destination}"
60
+ FileUtils.mv(file, destination)
61
+ puts "Moved #{file} to #{destination}"
62
+ end
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
5
+
6
+ require 'appydave/tools'
7
+
8
+ # Process command line arguments for SubtitleMaster operations
9
+ class SubtitleMasterCLI
10
+ def initialize
11
+ @commands = {
12
+ 'clean' => method(:clean_subtitles),
13
+ 'correct' => method(:correct_subtitles),
14
+ 'split' => method(:split_subtitles),
15
+ 'highlight' => method(:highlight_subtitles),
16
+ 'image_prompts' => method(:generate_image_prompts)
17
+ }
18
+ end
19
+
20
+ def run
21
+ command, *args = ARGV
22
+ if @commands.key?(command)
23
+ @commands[command].call(args)
24
+ else
25
+ puts "Unknown command: #{command}"
26
+ print_help
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def clean_subtitles(args)
33
+ options = parse_options(args, 'clean')
34
+ cleaner = Appydave::Tools::SubtitleMaster::Clean.new(file_path: options[:file])
35
+ result = cleaner.clean
36
+ write_output(result, options[:output])
37
+ end
38
+
39
+ def correct_subtitles(args)
40
+ options = parse_options(args, 'correct')
41
+ corrector = Appydave::Tools::SubtitleMaster::Correct.new(options[:file])
42
+ result = corrector.correct
43
+ write_output(result, options[:output])
44
+ end
45
+
46
+ def split_subtitles(args)
47
+ options = parse_options(args, 'split', %i[words_per_group])
48
+ splitter = Appydave::Tools::SubtitleMaster::Split.new(options[:file], options[:words_per_group])
49
+ result = splitter.split
50
+ write_output(result, options[:output])
51
+ end
52
+
53
+ def highlight_subtitles(args)
54
+ options = parse_options(args, 'highlight')
55
+ highlighter = Appydave::Tools::SubtitleMaster::Highlight.new(options[:file])
56
+ result = highlighter.highlight
57
+ write_output(result, options[:output])
58
+ end
59
+
60
+ def generate_image_prompts(args)
61
+ options = parse_options(args, 'image_prompts')
62
+ image_prompter = Appydave::Tools::SubtitleMaster::ImagePrompts.new(options[:file])
63
+ result = image_prompter.generate_prompts
64
+ write_output(result, options[:output])
65
+ end
66
+
67
+ def parse_options(args, command, extra_options = [])
68
+ options = { file: nil, output: nil }
69
+ OptionParser.new do |opts|
70
+ opts.banner = "Usage: subtitle_master.rb #{command} [options]"
71
+
72
+ opts.on('-f', '--file FILE', 'SRT file to process') { |v| options[:file] = v }
73
+ opts.on('-o', '--output FILE', 'Output file') { |v| options[:output] = v }
74
+
75
+ extra_options.each do |opt|
76
+ case opt
77
+ when :words_per_group
78
+ opts.on('-w', '--words-per-group WORDS', 'Number of words per group for splitting') { |v| options[:words_per_group] = v.to_i }
79
+ end
80
+ end
81
+
82
+ opts.on_tail('-h', '--help', 'Show this message') do
83
+ puts opts
84
+ exit
85
+ end
86
+ end.parse!(args)
87
+
88
+ unless options[:file] && options[:output]
89
+ puts 'Missing required options. Use -h for help.'
90
+ exit
91
+ end
92
+
93
+ options
94
+ end
95
+
96
+ def write_output(result, output_file)
97
+ File.write(output_file, result)
98
+ puts "Processed file written to #{output_file}"
99
+ end
100
+
101
+ def print_help
102
+ puts 'Usage: subtitle_master.rb [command] [options]'
103
+ puts 'Commands:'
104
+ puts ' clean Clean and normalize SRT files'
105
+ puts ' correct Correct common typos and mistranslations in SRT files'
106
+ puts ' split Split subtitle groups based on word count'
107
+ puts ' highlight Highlight power words in subtitles'
108
+ puts ' image_prompts Generate image prompts from subtitle text'
109
+ puts "Run 'subtitle_master.rb [command] --help' for more information on a command."
110
+ end
111
+ end
112
+
113
+ SubtitleMasterCLI.new.run
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
+ require 'optparse'
5
+
4
6
  $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
5
7
 
6
8
  require 'appydave/tools'
@@ -10,15 +12,18 @@ class SubtitleMasterCLI
10
12
  def initialize
11
13
  @commands = {
12
14
  'clean' => method(:clean_subtitles),
13
- 'correct' => method(:correct_subtitles),
14
- 'split' => method(:split_subtitles),
15
- 'highlight' => method(:highlight_subtitles),
16
- 'image_prompts' => method(:generate_image_prompts)
15
+ 'join' => method(:join_subtitles)
17
16
  }
18
17
  end
19
18
 
20
19
  def run
21
20
  command, *args = ARGV
21
+ if command.nil?
22
+ puts 'No command provided. Use -h for help.'
23
+ print_help
24
+ exit
25
+ end
26
+
22
27
  if @commands.key?(command)
23
28
  @commands[command].call(args)
24
29
  else
@@ -30,82 +35,116 @@ class SubtitleMasterCLI
30
35
  private
31
36
 
32
37
  def clean_subtitles(args)
33
- options = parse_options(args, 'clean')
38
+ options = { file: nil, output: nil }
39
+
40
+ # Command-specific option parser
41
+ clean_parser = OptionParser.new do |opts|
42
+ opts.banner = 'Usage: subtitle_master.rb clean [options]'
43
+
44
+ opts.on('-f', '--file FILE', 'SRT file to process') do |v|
45
+ options[:file] = v
46
+ end
47
+
48
+ opts.on('-o', '--output FILE', 'Output file') do |v|
49
+ options[:output] = v
50
+ end
51
+
52
+ opts.on('-h', '--help', 'Show this message') do
53
+ puts opts
54
+ exit
55
+ end
56
+ end
57
+
58
+ begin
59
+ clean_parser.parse!(args)
60
+ rescue OptionParser::InvalidOption => e
61
+ puts "Error: #{e.message}"
62
+ puts clean_parser
63
+ exit
64
+ end
65
+
66
+ # Validate required options
67
+ if options[:file].nil? || options[:output].nil?
68
+ puts 'Error: Missing required options.'
69
+ puts clean_parser
70
+ exit
71
+ end
72
+
73
+ # Assuming `Appydave::Tools::SubtitleMaster::Clean` exists
34
74
  cleaner = Appydave::Tools::SubtitleMaster::Clean.new(file_path: options[:file])
35
- result = cleaner.clean
36
- write_output(result, options[:output])
75
+ cleaner.clean
76
+ cleaner.write(options[:output])
37
77
  end
38
78
 
39
- def correct_subtitles(args)
40
- options = parse_options(args, 'correct')
41
- corrector = Appydave::Tools::SubtitleMaster::Correct.new(options[:file])
42
- result = corrector.correct
43
- write_output(result, options[:output])
44
- end
79
+ def join_subtitles(args)
80
+ options = {
81
+ folder: './',
82
+ files: '*.srt',
83
+ sort: 'inferred',
84
+ buffer: 100,
85
+ output: 'merged.srt',
86
+ verbose: false
87
+ }
45
88
 
46
- def split_subtitles(args)
47
- options = parse_options(args, 'split', %i[words_per_group])
48
- splitter = Appydave::Tools::SubtitleMaster::Split.new(options[:file], options[:words_per_group])
49
- result = splitter.split
50
- write_output(result, options[:output])
51
- end
89
+ join_parser = OptionParser.new do |opts|
90
+ opts.banner = 'Usage: subtitle_master.rb join [options]'
52
91
 
53
- def highlight_subtitles(args)
54
- options = parse_options(args, 'highlight')
55
- highlighter = Appydave::Tools::SubtitleMaster::Highlight.new(options[:file])
56
- result = highlighter.highlight
57
- write_output(result, options[:output])
58
- end
92
+ opts.on('-d', '--directory DIR', 'Directory containing SRT files (default: current directory)') do |v|
93
+ options[:folder] = v
94
+ end
59
95
 
60
- def generate_image_prompts(args)
61
- options = parse_options(args, 'image_prompts')
62
- image_prompter = Appydave::Tools::SubtitleMaster::ImagePrompts.new(options[:file])
63
- result = image_prompter.generate_prompts
64
- write_output(result, options[:output])
65
- end
96
+ opts.on('-f', '--files PATTERN', 'File pattern (e.g., "*.srt" or "part1.srt,part2.srt")') do |v|
97
+ options[:files] = v
98
+ end
66
99
 
67
- def parse_options(args, command, extra_options = [])
68
- options = { file: nil, output: nil }
69
- OptionParser.new do |opts|
70
- opts.banner = "Usage: subtitle_master.rb #{command} [options]"
100
+ opts.on('-s', '--sort ORDER', %w[asc desc inferred], 'Sort order (asc/desc/inferred)') do |v|
101
+ options[:sort] = v
102
+ end
71
103
 
72
- opts.on('-f', '--file FILE', 'SRT file to process') { |v| options[:file] = v }
73
- opts.on('-o', '--output FILE', 'Output file') { |v| options[:output] = v }
104
+ opts.on('-b', '--buffer MS', Integer, 'Buffer between merged files in milliseconds') do |v|
105
+ options[:buffer] = v
106
+ end
107
+
108
+ opts.on('-o', '--output FILE', 'Output file') do |v|
109
+ options[:output] = v
110
+ end
74
111
 
75
- extra_options.each do |opt|
76
- case opt
77
- when :words_per_group
78
- opts.on('-w', '--words-per-group WORDS', 'Number of words per group for splitting') { |v| options[:words_per_group] = v.to_i }
79
- end
112
+ opts.on('-L', '--log-level LEVEL', %w[none info detail], 'Log level (default: info)') do |v|
113
+ options[:log_level] = v.to_sym
80
114
  end
81
115
 
82
- opts.on_tail('-h', '--help', 'Show this message') do
116
+ opts.on('-h', '--help', 'Show this message') do
83
117
  puts opts
84
118
  exit
85
119
  end
86
- end.parse!(args)
120
+ end
87
121
 
88
- unless options[:file] && options[:output]
89
- puts 'Missing required options. Use -h for help.'
122
+ begin
123
+ join_parser.parse!(args)
124
+ rescue OptionParser::InvalidOption => e
125
+ puts "Error: #{e.message}"
126
+ puts join_parser
90
127
  exit
91
128
  end
92
129
 
93
- options
94
- end
130
+ # Validate required options
131
+ if options[:folder].nil? || options[:files].nil? || options[:output].nil?
132
+ puts 'Error: Missing required options.'
133
+ puts join_parser
134
+ exit
135
+ end
95
136
 
96
- def write_output(result, output_file)
97
- File.write(output_file, result)
98
- puts "Processed file written to #{output_file}"
137
+ # Assuming `Appydave::Tools::SubtitleMaster::Join` exists
138
+ joiner = Appydave::Tools::SubtitleMaster::Join.new(folder: options[:folder], files: options[:files], sort: options[:sort], buffer: options[:buffer], output: options[:output],
139
+ log_level: options[:log_level])
140
+ joiner.join
99
141
  end
100
142
 
101
143
  def print_help
102
144
  puts 'Usage: subtitle_master.rb [command] [options]'
103
145
  puts 'Commands:'
104
146
  puts ' clean Clean and normalize SRT files'
105
- puts ' correct Correct common typos and mistranslations in SRT files'
106
- puts ' split Split subtitle groups based on word count'
107
- puts ' highlight Highlight power words in subtitles'
108
- puts ' image_prompts Generate image prompts from subtitle text'
147
+ puts ' join Join multiple SRT files'
109
148
  puts "Run 'subtitle_master.rb [command] --help' for more information on a command."
110
149
  end
111
150
  end
@@ -0,0 +1,166 @@
1
+
2
+ # Detailed Requirements Specification
3
+
4
+ ## Command-Line Tool
5
+
6
+ ### Purpose
7
+ A CLI tool named `Join` for merging multiple SRT files into one cohesive subtitle file, handling timestamp adjustment and preserving subtitle integrity.
8
+
9
+ ---
10
+
11
+ ### Parameters
12
+
13
+ 1. **`--folder <path>`**
14
+ - **Purpose**: Specifies the folder containing SRT files.
15
+ - **Default**: Current working directory (`./`).
16
+ - **Example**: `--folder /path/to/subtitles`.
17
+
18
+ 2. **`--files <pattern>`**
19
+ - **Purpose**: Specifies specific filenames or wildcard patterns for SRT files to process.
20
+ - **Default**: `*.srt` (processes all SRT files in the folder).
21
+ - **Logic**:
22
+ - Explicit filenames: `file1.srt file2.srt` → Process in provided order.
23
+ - Wildcard patterns: `*.srt` → Resolve matching files, sort by `--sort` order.
24
+
25
+ 3. **`--sort <inferred|asc|desc>`**
26
+ - **Purpose**: Defines how files are ordered for processing.
27
+ - **Default**: `inferred`
28
+ - **Logic**:
29
+ - `inferred`:
30
+ - Explicit filenames → Preserve order.
31
+ - Wildcards → Sort alphabetically (ascending).
32
+ - `asc`: Force alphabetical order.
33
+ - `desc`: Force reverse alphabetical order.
34
+
35
+ 4. **`--buffer <milliseconds>`**
36
+ - **Purpose**: Adds a buffer between the last subtitle of one file and the first subtitle of the next.
37
+ - **Default**: `100` (100ms).
38
+ - **Example**: `--buffer 50` for a 50ms gap.
39
+
40
+ 5. **`--output <output_file.srt>`**
41
+ - **Purpose**: Specifies the name of the output merged SRT file.
42
+ - **Default**: `merged.srt` in the current folder.
43
+ - **Example**: `--output /path/to/final_output.srt`.
44
+
45
+ 6. **`--verbose`**
46
+ - **Purpose**: Enables detailed logging for steps like file resolution, timestamp adjustments, and warnings for skipped files.
47
+
48
+ ---
49
+
50
+ ## Primary Component Logic
51
+
52
+ ### Design Overview
53
+ The tool should adhere to **Single Responsibility Principle (SRP)**, favor **composition**, and nest the composed components within the `Join` class for encapsulation and reusability.
54
+
55
+ ### Primary Class: `Join`
56
+ The `Join` class acts as the main entry point and coordinates the workflow. It contains the following nested components:
57
+
58
+ 1. **`FileResolver`**
59
+ - **Responsibility**: Handles file resolution (folder, filenames, wildcards, sorting).
60
+ - **Logic**:
61
+ - Resolve files based on `--files` or `--folder`.
62
+ - Apply sorting rules (`inferred`, `asc`, `desc`).
63
+
64
+ 2. **`SRTParser`**
65
+ - **Responsibility**: Parses SRT files into structured subtitle objects.
66
+ - **Logic**:
67
+ - Read and validate SRT content.
68
+ - Split into subtitle blocks (index, start time, end time, text).
69
+ - Convert timestamps into structured objects (seconds for calculations).
70
+
71
+ 3. **`SRTMerger`**
72
+ - **Responsibility**: Combines subtitles, adjusts timestamps, and applies buffers.
73
+ - **Logic**:
74
+ - Aggregate parsed subtitles from multiple files.
75
+ - Adjust timestamps for non-overlapping entries using the buffer.
76
+
77
+ 4. **`SRTWriter`**
78
+ - **Responsibility**: Converts subtitle objects back to SRT format and writes to disk.
79
+ - **Logic**:
80
+ - Reformat subtitles with proper numbering and timestamp formatting.
81
+ - Write the final merged content to the specified output file.
82
+
83
+ ---
84
+
85
+ ### Main Workflow
86
+
87
+ 1. **Parse Arguments**
88
+ - Validate input parameters and resolve defaults.
89
+
90
+ 2. **Resolve Files**
91
+ - Use `FileResolver` to identify files based on folder, filenames, or wildcard patterns.
92
+
93
+ 3. **Parse Files**
94
+ - Use `SRTParser` to parse each file into a list of structured subtitle objects.
95
+
96
+ 4. **Merge Files**
97
+ - Use `SRTMerger` to combine subtitles, sort by start time (per file order), and apply timestamp adjustments.
98
+
99
+ 5. **Write Output**
100
+ - Use `SRTWriter` to generate the final SRT file and save it to the specified location.
101
+
102
+ ---
103
+
104
+ ## Business Rules
105
+
106
+ 1. **File Selection**
107
+ - Resolve wildcards to file lists dynamically.
108
+ - Validate file existence and skip invalid or non-SRT files with appropriate warnings.
109
+
110
+ 2. **Timestamp Handling**
111
+ - Ensure non-overlapping subtitles across files by applying the buffer.
112
+ - Maintain relative timing within each file.
113
+
114
+ 3. **Sorting Logic**
115
+ - Preserve file order for explicit filenames.
116
+ - Apply alphabetical sorting for wildcards unless overridden by `--sort`.
117
+
118
+ 4. **Error Handling**
119
+ - Log and skip malformed SRT files.
120
+ - Notify users of missing files or unsupported patterns.
121
+
122
+ 5. **Output Consistency**
123
+ - Re-number subtitles sequentially in the final output.
124
+ - Ensure proper formatting with three decimal places for milliseconds.
125
+
126
+ ---
127
+
128
+ ## Tests
129
+
130
+ ### Unit Tests
131
+
132
+ 1. **FileResolver Tests**
133
+ - Resolve explicit filenames.
134
+ - Resolve wildcard patterns (`*.srt`) and sort files.
135
+ - Handle missing or invalid files gracefully.
136
+
137
+ 2. **SRTParser Tests**
138
+ - Parse valid SRT files into structured objects.
139
+ - Detect and handle malformed SRT files.
140
+ - Validate timestamp formatting.
141
+
142
+ 3. **SRTMerger Tests**
143
+ - Combine subtitles from two or more files.
144
+ - Ensure timestamps are adjusted with the buffer.
145
+ - Handle edge cases like overlapping timestamps or missing buffer.
146
+
147
+ 4. **SRTWriter Tests**
148
+ - Convert subtitle objects to valid SRT format.
149
+ - Ensure proper numbering and formatting (e.g., three decimal places).
150
+
151
+ ---
152
+
153
+ ### Integration Tests
154
+
155
+ 1. Process a folder of SRT files and produce a merged output.
156
+ 2. Validate behavior with explicit filenames and wildcards.
157
+ 3. Verify handling of buffers for different values (`0ms`, `50ms`, etc.).
158
+ 4. Test edge cases like empty files, malformed SRTs, or mixed valid/invalid files.
159
+
160
+ ---
161
+
162
+ ### End-to-End Tests
163
+
164
+ 1. Run the tool with minimal arguments and verify the output.
165
+ 2. Test all parameter combinations (`--folder`, `--files`, `--sort`, `--buffer`, etc.).
166
+ 3. Compare merged output against manually validated reference files.
@@ -5,6 +5,8 @@ module Appydave
5
5
  module SubtitleMaster
6
6
  # Clean and normalize subtitles
7
7
  class Clean
8
+ attr_reader :content
9
+
8
10
  def initialize(file_path: nil, srt_content: nil)
9
11
  if file_path && srt_content
10
12
  raise ArgumentError, 'You cannot provide both a file path and an SRT content stream.'
@@ -24,6 +26,15 @@ module Appydave
24
26
  normalize_lines(content)
25
27
  end
26
28
 
29
+ def write(output_file)
30
+ File.write(output_file, content)
31
+ puts "Processed file written to #{output_file}"
32
+ rescue Errno::EACCES
33
+ puts "Permission denied: Unable to write to #{output_file}"
34
+ rescue StandardError => e
35
+ puts "An error occurred while writing to the file: #{e.message}"
36
+ end
37
+
27
38
  private
28
39
 
29
40
  def remove_underscores(content)
@@ -0,0 +1,327 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ module SubtitleMaster
6
+ # Join multiple SRT files into one
7
+ # - Supports folder, wildcards, sorting via FileResolver
8
+ class Join
9
+ # Handles file resolution (folder, wildcards, sorting)
10
+ class FileResolver
11
+ def initialize(folder:, files:, sort:)
12
+ raise ArgumentError, 'folder is required' if folder.nil?
13
+ raise ArgumentError, 'files is required' if files.nil?
14
+
15
+ @folder = folder
16
+ @files = files
17
+ @sort = sort
18
+ end
19
+
20
+ def process
21
+ # Check if folder exists before processing
22
+ raise Errno::ENOENT, "No such directory - #{@folder}" unless Dir.exist?(@folder)
23
+
24
+ file_patterns = @files.split(',').map(&:strip)
25
+ resolved_files = file_patterns.flat_map { |pattern| resolve_pattern(pattern) }
26
+ sort_files(resolved_files)
27
+ end
28
+
29
+ private
30
+
31
+ def resolve_pattern(pattern)
32
+ if pattern.include?('*')
33
+ Dir.glob(File.join(@folder, pattern))
34
+ else
35
+ file_path = File.join(@folder, pattern)
36
+ File.exist?(file_path) ? [file_path] : []
37
+ end
38
+ end
39
+
40
+ def sort_files(files)
41
+ case @sort
42
+ when 'asc'
43
+ files.sort
44
+ when 'desc'
45
+ files.sort.reverse
46
+ else # 'inferred'
47
+ # If explicit files were provided (no wildcards), maintain order
48
+ return files unless @files.include?('*')
49
+
50
+ files.sort
51
+ end
52
+ end
53
+ end
54
+
55
+ # Parses SRT files into structured subtitle objects
56
+ class SRTParser
57
+ # Represents a single subtitle entry
58
+ class Subtitle
59
+ attr_reader :index, :start_time, :end_time, :text
60
+
61
+ def initialize(index:, start_time:, end_time:, text:)
62
+ @index = index
63
+ @start_time = parse_timestamp(start_time)
64
+ @end_time = parse_timestamp(end_time)
65
+ @text = text.strip
66
+ end
67
+
68
+ private
69
+
70
+ # Converts SRT timestamp (00:00:00,000) to seconds (float)
71
+ def parse_timestamp(timestamp)
72
+ hours, minutes, seconds_ms = timestamp.split(':')
73
+ seconds, milliseconds = seconds_ms.split(',')
74
+
75
+ (hours.to_i * 3600) +
76
+ (minutes.to_i * 60) +
77
+ seconds.to_i +
78
+ (milliseconds.to_i / 1000.0)
79
+ end
80
+ end
81
+
82
+ def parse(content)
83
+ validate_content!(content)
84
+
85
+ subtitles = []
86
+ current_block = { text: [] }
87
+
88
+ content.split("\n").each do |line|
89
+ line = line.strip
90
+
91
+ if line.empty?
92
+ process_block(current_block, subtitles) if current_block[:index]
93
+ current_block = { text: [] }
94
+ next
95
+ end
96
+
97
+ if current_block[:index].nil?
98
+ current_block[:index] = line.to_i
99
+ elsif current_block[:timestamp].nil? && line.include?(' --> ')
100
+ start_time, end_time = line.split(' --> ')
101
+ current_block[:timestamp] = { start: start_time, end: end_time }
102
+ else
103
+ current_block[:text] << line
104
+ end
105
+ end
106
+
107
+ # Process the last block if it exists
108
+ process_block(current_block, subtitles) if current_block[:index]
109
+
110
+ subtitles
111
+ end
112
+
113
+ private
114
+
115
+ def validate_content!(content)
116
+ raise ArgumentError, 'Content cannot be nil' if content.nil?
117
+ raise ArgumentError, 'Content cannot be empty' if content.strip.empty?
118
+
119
+ # Basic structure validation - should have numbers and timestamps
120
+ return if content.match?(/\d+\s*\n\d{2}:\d{2}:\d{2},\d{3}\s*-->\s*\d{2}:\d{2}:\d{2},\d{3}/)
121
+
122
+ raise ArgumentError, 'Invalid SRT format: missing required timestamp format'
123
+ end
124
+
125
+ def process_block(block, subtitles)
126
+ return unless block[:index] && block[:timestamp] && !block[:text].empty?
127
+
128
+ subtitles << Subtitle.new(
129
+ index: block[:index],
130
+ start_time: block[:timestamp][:start],
131
+ end_time: block[:timestamp][:end],
132
+ text: block[:text].join("\n")
133
+ )
134
+ end
135
+ end
136
+
137
+ # Merges multiple subtitle arrays while maintaining timing and adding buffers
138
+ class SRTMerger
139
+ def initialize(buffer_ms: 100)
140
+ @buffer_ms = buffer_ms.to_f
141
+ end
142
+
143
+ def merge(subtitle_arrays)
144
+ return [] if subtitle_arrays.empty?
145
+
146
+ merged = []
147
+ current_end_time = 0.0
148
+
149
+ subtitle_arrays.each do |subtitles|
150
+ next if subtitles.empty?
151
+
152
+ # Calculate offset needed for this batch of subtitles
153
+ first_subtitle = subtitles.first
154
+ offset_seconds = calculate_offset(current_end_time, first_subtitle.start_time)
155
+
156
+ # Add adjusted subtitles to merged array
157
+ subtitles.each do |subtitle|
158
+ adjusted_subtitle = adjust_subtitle_timing(subtitle, offset_seconds)
159
+ merged << adjusted_subtitle
160
+ end
161
+
162
+ # Update current_end_time for next batch
163
+ current_end_time = merged.last.end_time
164
+ end
165
+
166
+ # Renumber subtitles sequentially
167
+ merged.each_with_index do |subtitle, index|
168
+ subtitle.instance_variable_set(:@index, index + 1)
169
+ end
170
+
171
+ merged
172
+ end
173
+
174
+ private
175
+
176
+ def calculate_offset(current_end_time, next_start_time)
177
+ return 0.0 if current_end_time.zero?
178
+
179
+ buffer_seconds = @buffer_ms / 1000.0
180
+ needed_offset = current_end_time + buffer_seconds - next_start_time
181
+ [needed_offset, 0].max
182
+ end
183
+
184
+ def adjust_subtitle_timing(subtitle, offset_seconds)
185
+ # Create new Subtitle instance with adjusted timing
186
+ SRTParser::Subtitle.new(
187
+ index: subtitle.index,
188
+ start_time: format_time(subtitle.start_time + offset_seconds),
189
+ end_time: format_time(subtitle.end_time + offset_seconds),
190
+ text: subtitle.text
191
+ )
192
+ end
193
+
194
+ def format_time(seconds)
195
+ # Convert seconds back to SRT timestamp format (00:00:00,000)
196
+ hours = (seconds / 3600).floor
197
+ minutes = ((seconds % 3600) / 60).floor
198
+ seconds_remaining = seconds % 60
199
+ milliseconds = ((seconds_remaining % 1) * 1000).round
200
+
201
+ format(
202
+ '%<hours>02d:%<minutes>02d:%<seconds>02d,%<milliseconds>03d',
203
+ hours: hours,
204
+ minutes: minutes,
205
+ seconds: seconds_remaining.floor,
206
+ milliseconds: milliseconds
207
+ )
208
+ end
209
+ end
210
+
211
+ # Converts subtitle objects back to SRT format and writes to disk
212
+ class SRTWriter
213
+ def initialize(output_file)
214
+ @output_file = output_file
215
+ end
216
+
217
+ def write(subtitles)
218
+ content = format_subtitles(subtitles)
219
+ File.write(@output_file, content, encoding: 'UTF-8')
220
+ end
221
+
222
+ private
223
+
224
+ def format_subtitles(subtitles)
225
+ subtitles.each_with_index.map do |subtitle, index|
226
+ [
227
+ index + 1, # Force sequential numbering
228
+ format_timestamp_line(subtitle),
229
+ subtitle.text,
230
+ '' # Empty line between subtitle blocks
231
+ ].join("\n")
232
+ end.join("\n")
233
+ end
234
+
235
+ def format_timestamp_line(subtitle)
236
+ "#{format_timestamp(subtitle.start_time)} --> #{format_timestamp(subtitle.end_time)}"
237
+ end
238
+
239
+ def format_timestamp(seconds)
240
+ hours = (seconds / 3600).floor
241
+ minutes = ((seconds % 3600) / 60).floor
242
+ seconds_remaining = seconds % 60
243
+ milliseconds = ((seconds_remaining % 1) * 1000).round
244
+
245
+ format(
246
+ '%<hours>02d:%<minutes>02d:%<seconds>02d,%<milliseconds>03d',
247
+ hours: hours,
248
+ minutes: minutes,
249
+ seconds: seconds_remaining.floor,
250
+ milliseconds: milliseconds
251
+ )
252
+ end
253
+ end
254
+
255
+ # Simple logger for debugging
256
+ class Logger
257
+ LEVELS = { none: 0, info: 1, detail: 2 }.freeze
258
+
259
+ def initialize(level = :info)
260
+ @level = LEVELS[level] || LEVELS[:info]
261
+ end
262
+
263
+ def log(level, message)
264
+ puts message if LEVELS[level] <= @level
265
+ end
266
+ end
267
+
268
+ def initialize(folder: './', files: '*.srt', sort: 'inferred', buffer: 100, output: 'merged.srt', log_level: :info)
269
+ @folder = folder
270
+ @files = files
271
+ @sort = sort
272
+ @buffer = buffer
273
+ @output = output
274
+ @logger = Logger.new(log_level)
275
+ end
276
+
277
+ def join
278
+ @logger.log(:info, "Starting join operation in folder: #{@folder} with files: #{@files}")
279
+ resolved_files = resolve_files
280
+ @logger.log(:info, "Resolved files: #{resolved_files.join(', ')}")
281
+
282
+ subtitle_groups = parse_files(resolved_files)
283
+ @logger.log(:detail, "Parsed subtitles: #{subtitle_groups.map(&:size)} from files.")
284
+
285
+ merged_subtitles = merge_subtitles(subtitle_groups)
286
+ @logger.log(:info, "Merged #{subtitle_groups.flatten.size} subtitles into #{merged_subtitles.size} blocks.")
287
+
288
+ write_output(merged_subtitles)
289
+ @logger.log(:info, "Output written to #{@output}")
290
+ end
291
+
292
+ private
293
+
294
+ def resolve_files
295
+ resolver = FileResolver.new(
296
+ folder: @folder,
297
+ files: @files,
298
+ sort: @sort
299
+ )
300
+ resolver.process
301
+ end
302
+
303
+ def parse_files(files)
304
+ files.map do |file|
305
+ content = File.read(file, encoding: 'UTF-8')
306
+ parse_srt_content(content)
307
+ end
308
+ end
309
+
310
+ def parse_srt_content(content)
311
+ parser = SRTParser.new
312
+ parser.parse(content)
313
+ end
314
+
315
+ def merge_subtitles(subtitle_groups)
316
+ merger = SRTMerger.new(buffer_ms: @buffer)
317
+ merger.merge(subtitle_groups)
318
+ end
319
+
320
+ def write_output(subtitles)
321
+ writer = SRTWriter.new(@output)
322
+ writer.write(subtitles)
323
+ end
324
+ end
325
+ end
326
+ end
327
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Appydave
4
4
  module Tools
5
- VERSION = '0.11.10'
5
+ VERSION = '0.12.0'
6
6
  end
7
7
  end
@@ -44,18 +44,14 @@ require 'appydave/tools/configuration/configurable'
44
44
  require 'appydave/tools/configuration/config'
45
45
  require 'appydave/tools/configuration/models/config_base'
46
46
  require 'appydave/tools/configuration/models/settings_config'
47
- # require 'appydave/tools/configuration/models/bank_reconciliation_config'
48
47
  require 'appydave/tools/configuration/models/channels_config'
49
48
  require 'appydave/tools/configuration/models/youtube_automation_config'
50
49
  require 'appydave/tools/name_manager/project_name'
51
- # require 'appydave/tools/bank_reconciliation/clean/clean_transactions'
52
- # require 'appydave/tools/bank_reconciliation/clean/read_transactions'
53
- # require 'appydave/tools/bank_reconciliation/clean/mapper'
54
- # require 'appydave/tools/bank_reconciliation/models/transaction'
55
50
 
56
51
  require 'appydave/tools/prompt_tools/prompt_completion'
57
52
 
58
53
  require 'appydave/tools/subtitle_master/clean'
54
+ require 'appydave/tools/subtitle_master/join'
59
55
 
60
56
  require 'appydave/tools/youtube_automation/gpt_agent'
61
57
 
data/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "appydave-tools",
3
- "version": "0.11.10",
3
+ "version": "0.12.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "appydave-tools",
9
- "version": "0.11.10",
9
+ "version": "0.12.0",
10
10
  "devDependencies": {
11
11
  "@klueless-js/semantic-release-rubygem": "github:klueless-js/semantic-release-rubygem",
12
12
  "@semantic-release/changelog": "^6.0.3",
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appydave-tools",
3
- "version": "0.11.10",
3
+ "version": "0.12.0",
4
4
  "description": "AppyDave YouTube Automation Tools",
5
5
  "scripts": {
6
6
  "release": "semantic-release"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appydave-tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.10
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Cruwys
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-11-26 00:00:00.000000000 Z
11
+ date: 2024-12-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -160,8 +160,10 @@ files:
160
160
  - bin/configuration.rb
161
161
  - bin/console
162
162
  - bin/gpt_context.rb
163
+ - bin/move_images.rb
163
164
  - bin/prompt_tools.rb
164
165
  - bin/setup
166
+ - bin/subtitle_master-old.rb
165
167
  - bin/subtitle_master.rb
166
168
  - bin/youtube_automation.rb
167
169
  - bin/youtube_manager.rb
@@ -193,9 +195,11 @@ files:
193
195
  - lib/appydave/tools/name_manager/project_name.rb
194
196
  - lib/appydave/tools/prompt_tools/_doc.md
195
197
  - lib/appydave/tools/prompt_tools/prompt_completion.rb
198
+ - lib/appydave/tools/subtitle_master/_doc-clean.md
199
+ - lib/appydave/tools/subtitle_master/_doc-join.md
196
200
  - lib/appydave/tools/subtitle_master/_doc-todo.md
197
- - lib/appydave/tools/subtitle_master/_doc.md
198
201
  - lib/appydave/tools/subtitle_master/clean.rb
202
+ - lib/appydave/tools/subtitle_master/join.rb
199
203
  - lib/appydave/tools/types/array_type.rb
200
204
  - lib/appydave/tools/types/base_model.rb
201
205
  - lib/appydave/tools/types/hash_type.rb