appydave-tools 0.11.11 → 0.13.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: e4596ba8f5d43c23761a5375cb0329b89ca6b18bb32a03686fd3427d4a73086b
4
- data.tar.gz: 2a185916599f6c9d2578345b08bbc5e845fd0642f9bebab8d70c1b94ea4cf0cf
3
+ metadata.gz: 17dec612ce270c5ffa25c2173a96b437b85ccd6dfe1e1d2eb913060a4ebc54c1
4
+ data.tar.gz: 6b280aebe5245dc5c619169aa75638c9be027048736c28f4f8f3baeb33dccc65
5
5
  SHA512:
6
- metadata.gz: 5f4a96ad81115e20d39615547a012d58a5c6925d6f0841d31f2f91265d0412e937610229fde478e65b529c5b8cef437eed2d22f1152c4e2ad42cfa5fb45c8983
7
- data.tar.gz: ae08e2209f871885fd94ae69530b1760cdf879dec9ac185cefb467e10a6bf5873fcbd33fe648f8f6c14603abe5962b7f00a15d8e23a8105aeef83a44d7cbac29
6
+ metadata.gz: 143d0c023e0ffd5dd86086e5f34f434216b7f3ed10c129e6971a9e0144eae3a36f631fc2ffbbd4a1b6ad290fdcb9e1ce5032810b65087bcfbc724799a7812c96
7
+ data.tar.gz: 9a26010a6d0f7936b094e2f1e5b4391e96d0f8a7bb58f686d6e7ca03100f7eb67da342a92f046499240c127d718bfa08ca41e07c1c3909c3fdd83b47827ecdc2
data/.rubocop.yml CHANGED
@@ -91,6 +91,7 @@ Metrics/ParameterLists:
91
91
  Exclude:
92
92
  - "**/spec/**/*"
93
93
  - "**/models/**/*"
94
+ - "lib/appydave/tools/subtitle_manager/join.rb"
94
95
  Layout/EmptyLineBetweenDefs:
95
96
  Exclude:
96
97
  - "**/spec/**/*"
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ # [0.12.0](https://github.com/appydave/appydave-tools/compare/v0.11.11...v0.12.0) (2024-12-03)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * update cops ([0efed07](https://github.com/appydave/appydave-tools/commit/0efed07376a7fa8d460a3ab03264a82764245ebe))
7
+
8
+
9
+ ### Features
10
+
11
+ * add srt-join tool ([0576ed4](https://github.com/appydave/appydave-tools/commit/0576ed44f330869760832875126dd1f4c2bfcbd1))
12
+
13
+ ## [0.11.11](https://github.com/appydave/appydave-tools/compare/v0.11.10...v0.11.11) (2024-11-26)
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * update clean srt specs ([e0da656](https://github.com/appydave/appydave-tools/commit/e0da656a3bf04f094bc3726a588a59d99d3d24a8))
19
+
1
20
  ## [0.11.10](https://github.com/appydave/appydave-tools/compare/v0.11.9...v0.11.10) (2024-11-26)
2
21
 
3
22
 
@@ -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
@@ -67,7 +67,7 @@ class SubtitleMasterCLI
67
67
  def parse_options(args, command, extra_options = [])
68
68
  options = { file: nil, output: nil }
69
69
  OptionParser.new do |opts|
70
- opts.banner = "Usage: subtitle_master.rb #{command} [options]"
70
+ opts.banner = "Usage: subtitle_manager.rb #{command} [options]"
71
71
 
72
72
  opts.on('-f', '--file FILE', 'SRT file to process') { |v| options[:file] = v }
73
73
  opts.on('-o', '--output FILE', 'Output file') { |v| options[:output] = v }
@@ -99,14 +99,14 @@ class SubtitleMasterCLI
99
99
  end
100
100
 
101
101
  def print_help
102
- puts 'Usage: subtitle_master.rb [command] [options]'
102
+ puts 'Usage: subtitle_manager.rb [command] [options]'
103
103
  puts 'Commands:'
104
104
  puts ' clean Clean and normalize SRT files'
105
105
  puts ' correct Correct common typos and mistranslations in SRT files'
106
106
  puts ' split Split subtitle groups based on word count'
107
107
  puts ' highlight Highlight power words in subtitles'
108
108
  puts ' image_prompts Generate image prompts from subtitle text'
109
- puts "Run 'subtitle_master.rb [command] --help' for more information on a command."
109
+ puts "Run 'subtitle_manager.rb [command] --help' for more information on a command."
110
110
  end
111
111
  end
112
112
 
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'optparse'
5
+
6
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
7
+
8
+ require 'appydave/tools'
9
+
10
+ # Process command line arguments for SubtitleMaster operations
11
+ class SubtitleMasterCLI
12
+ def initialize
13
+ @commands = {
14
+ 'clean' => method(:clean_subtitles),
15
+ 'join' => method(:join_subtitles)
16
+ }
17
+ end
18
+
19
+ def run
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
+
27
+ if @commands.key?(command)
28
+ @commands[command].call(args)
29
+ else
30
+ puts "Unknown command: #{command}"
31
+ print_help
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def clean_subtitles(args)
38
+ options = { file: nil, output: nil }
39
+
40
+ # Command-specific option parser
41
+ clean_parser = OptionParser.new do |opts|
42
+ opts.banner = 'Usage: subtitle_manager.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
74
+ cleaner = Appydave::Tools::SubtitleMaster::Clean.new(file_path: options[:file])
75
+ cleaner.clean
76
+ cleaner.write(options[:output])
77
+ end
78
+
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
+ }
88
+
89
+ join_parser = OptionParser.new do |opts|
90
+ opts.banner = 'Usage: subtitle_manager.rb join [options]'
91
+
92
+ opts.on('-d', '--directory DIR', 'Directory containing SRT files (default: current directory)') do |v|
93
+ options[:folder] = v
94
+ end
95
+
96
+ opts.on('-f', '--files PATTERN', 'File pattern (e.g., "*.srt" or "part1.srt,part2.srt")') do |v|
97
+ options[:files] = v
98
+ end
99
+
100
+ opts.on('-s', '--sort ORDER', %w[asc desc inferred], 'Sort order (asc/desc/inferred)') do |v|
101
+ options[:sort] = v
102
+ end
103
+
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
111
+
112
+ opts.on('-L', '--log-level LEVEL', %w[none info detail], 'Log level (default: info)') do |v|
113
+ options[:log_level] = v.to_sym
114
+ end
115
+
116
+ opts.on('-h', '--help', 'Show this message') do
117
+ puts opts
118
+ exit
119
+ end
120
+ end
121
+
122
+ begin
123
+ join_parser.parse!(args)
124
+ rescue OptionParser::InvalidOption => e
125
+ puts "Error: #{e.message}"
126
+ puts join_parser
127
+ exit
128
+ end
129
+
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
136
+
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
141
+ end
142
+
143
+ def print_help
144
+ puts 'Usage: subtitle_manager.rb [command] [options]'
145
+ puts 'Commands:'
146
+ puts ' clean Clean and normalize SRT files'
147
+ puts ' join Join multiple SRT files'
148
+ puts "Run 'subtitle_manager.rb [command] --help' for more information on a command."
149
+ end
150
+ end
151
+
152
+ SubtitleMasterCLI.new.run
@@ -31,10 +31,10 @@ cleaner = SubtitleMaster::Clean.new(srt_content: srt_content_string)
31
31
  ### Command Line Usage
32
32
 
33
33
  ```bash
34
- ./bin/subtitle_master.rb clean -f path/to/example.srt -o path/to/example_cleaned.srt
34
+ ./bin/subtitle_manager.rb clean -f path/to/example.srt -o path/to/example_cleaned.srt
35
35
 
36
36
  # Example using alias
37
- ad_subtitle_master clean -f transcript/a45-banned-from-midjourney-16-alternatives.srt -o a45-transcript.srt
37
+ ad_subtitle_manager clean -f transcript/a45-banned-from-midjourney-16-alternatives.srt -o a45-transcript.srt
38
38
  ```
39
39
 
40
40
  This component reads the SRT file, processes the content to remove tags and normalize lines, and outputs a cleaned and formatted subtitle file that is easier to read and upload to platforms.
@@ -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.11'
5
+ VERSION = '0.13.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
- require 'appydave/tools/subtitle_master/clean'
53
+ require 'appydave/tools/subtitle_manager/clean'
54
+ require 'appydave/tools/subtitle_manager/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.11",
3
+ "version": "0.13.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "appydave-tools",
9
- "version": "0.11.11",
9
+ "version": "0.13.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.11",
3
+ "version": "0.13.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.11
4
+ version: 0.13.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,9 +160,11 @@ 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
165
- - bin/subtitle_master.rb
166
+ - bin/subtitle_manager-old.rb
167
+ - bin/subtitle_manager.rb
166
168
  - bin/youtube_automation.rb
167
169
  - bin/youtube_manager.rb
168
170
  - docs/usage/gpt-context.md
@@ -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
196
- - lib/appydave/tools/subtitle_master/_doc-todo.md
197
- - lib/appydave/tools/subtitle_master/_doc.md
198
- - lib/appydave/tools/subtitle_master/clean.rb
198
+ - lib/appydave/tools/subtitle_manager/_doc-clean.md
199
+ - lib/appydave/tools/subtitle_manager/_doc-join.md
200
+ - lib/appydave/tools/subtitle_manager/_doc-todo.md
201
+ - lib/appydave/tools/subtitle_manager/clean.rb
202
+ - lib/appydave/tools/subtitle_manager/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