appydave-tools 0.15.0 → 0.17.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -0
  3. data/AGENTS.md +22 -0
  4. data/CHANGELOG.md +19 -0
  5. data/CLAUDE.md +318 -79
  6. data/README.md +390 -81
  7. data/bin/archive_project.rb +249 -0
  8. data/bin/configuration.rb +21 -1
  9. data/bin/generate_manifest.rb +357 -0
  10. data/bin/subtitle_manager.rb +18 -12
  11. data/bin/subtitle_processor.rb +158 -0
  12. data/bin/sync_from_ssd.rb +236 -0
  13. data/bin/vat +623 -0
  14. data/docs/README.md +169 -0
  15. data/docs/archive/codebase-audit-2025-01.md +424 -0
  16. data/docs/archive/documentation-framework-proposal.md +808 -0
  17. data/docs/archive/purpose-and-philosophy.md +110 -0
  18. data/docs/archive/test-coverage-quick-wins.md +342 -0
  19. data/docs/archive/tool-discovery.md +199 -0
  20. data/docs/archive/tool-documentation-analysis.md +592 -0
  21. data/docs/configuration/.env.example +19 -0
  22. data/docs/configuration/README.md +394 -0
  23. data/docs/configuration/channels.example.json +26 -0
  24. data/docs/configuration/settings.example.json +6 -0
  25. data/docs/development/CODEX-recommendations.md +123 -0
  26. data/docs/development/README.md +100 -0
  27. data/docs/development/cli-architecture-patterns.md +1604 -0
  28. data/docs/development/pattern-comparison.md +284 -0
  29. data/docs/prd-unified-brands-configuration.md +792 -0
  30. data/docs/project-brand-systems-analysis.md +934 -0
  31. data/docs/tools/bank-reconciliation.md +269 -0
  32. data/docs/tools/cli-actions.md +444 -0
  33. data/docs/tools/configuration.md +329 -0
  34. data/docs/{usage → tools}/gpt-context.md +118 -7
  35. data/docs/tools/index.md +324 -0
  36. data/docs/tools/move-images.md +295 -0
  37. data/docs/tools/name-manager.md +322 -0
  38. data/docs/tools/prompt-tools.md +209 -0
  39. data/docs/tools/subtitle-processor.md +242 -0
  40. data/docs/tools/youtube-automation.md +258 -0
  41. data/docs/tools/youtube-manager.md +248 -0
  42. data/docs/vat/dam-vision.md +123 -0
  43. data/docs/vat/session-summary-2025-11-09.md +297 -0
  44. data/docs/vat/usage.md +508 -0
  45. data/docs/vat/vat-testing-plan.md +801 -0
  46. data/lib/appydave/tools/configuration/models/brands_config.rb +238 -0
  47. data/lib/appydave/tools/configuration/models/config_base.rb +7 -0
  48. data/lib/appydave/tools/configuration/models/settings_config.rb +4 -0
  49. data/lib/appydave/tools/{subtitle_manager → subtitle_processor}/clean.rb +1 -1
  50. data/lib/appydave/tools/{subtitle_manager → subtitle_processor}/join.rb +5 -2
  51. data/lib/appydave/tools/vat/config.rb +153 -0
  52. data/lib/appydave/tools/vat/config_loader.rb +91 -0
  53. data/lib/appydave/tools/vat/manifest_generator.rb +239 -0
  54. data/lib/appydave/tools/vat/project_listing.rb +198 -0
  55. data/lib/appydave/tools/vat/project_resolver.rb +132 -0
  56. data/lib/appydave/tools/vat/s3_operations.rb +560 -0
  57. data/lib/appydave/tools/version.rb +1 -1
  58. data/lib/appydave/tools.rb +11 -5
  59. data/package.json +1 -1
  60. metadata +85 -14
  61. data/docs/dam/overview.md +0 -28
  62. data/lib/mj-paste-test/main.rb +0 -35
  63. data/lib/mj-paste-test/prompts.txt +0 -18
  64. data/lib/mj-paste-test/readme-leonardo.md +0 -0
  65. /data/lib/appydave/tools/{subtitle_manager → subtitle_processor}/_doc-clean.md +0 -0
  66. /data/lib/appydave/tools/{subtitle_manager → subtitle_processor}/_doc-join.md +0 -0
  67. /data/lib/appydave/tools/{subtitle_manager → subtitle_processor}/_doc-todo.md +0 -0
@@ -0,0 +1,158 @@
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 SubtitleProcessor operations
11
+ class SubtitleProcessorCLI
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_processor.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::SubtitleProcessor::Clean` exists
74
+ cleaner = Appydave::Tools::SubtitleProcessor::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_processor.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::SubtitleProcessor::Join` exists
138
+ joiner = Appydave::Tools::SubtitleProcessor::Join.new(
139
+ folder: options[:folder],
140
+ files: options[:files],
141
+ sort: options[:sort],
142
+ buffer: options[:buffer],
143
+ output: options[:output],
144
+ log_level: options[:log_level]
145
+ )
146
+ joiner.join
147
+ end
148
+
149
+ def print_help
150
+ puts 'Usage: subtitle_processor.rb [command] [options]'
151
+ puts 'Commands:'
152
+ puts ' clean Clean and normalize SRT files'
153
+ puts ' join Join multiple SRT files'
154
+ puts "Run 'subtitle_processor.rb [command] --help' for more information on a command."
155
+ end
156
+ end
157
+
158
+ SubtitleProcessorCLI.new.run
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ # rubocop:disable all
4
+
5
+ # Sync non-video files from SSD to local video directory
6
+ # Reads projects.json manifest and syncs only projects not in local flat structure
7
+ # Copies transcripts, thumbnails, and documentation while excluding video files
8
+ #
9
+ # Usage: ruby sync_from_ssd.rb [--dry-run]
10
+
11
+ require 'fileutils'
12
+ require 'json'
13
+
14
+ # Load configuration
15
+ SCRIPT_DIR = File.dirname(__FILE__)
16
+ TOOLS_DIR = File.expand_path(File.join(SCRIPT_DIR, '..'))
17
+ require_relative '../lib/config_loader'
18
+
19
+ # Determine paths relative to current working directory (repo root)
20
+ LOCAL_BASE = Dir.pwd
21
+ LOCAL_ARCHIVED = File.join(LOCAL_BASE, 'archived')
22
+ MANIFEST_FILE = File.join(LOCAL_BASE, 'projects.json')
23
+
24
+ # Load SSD_BASE from config
25
+ begin
26
+ config = ConfigLoader.load_from_repo(LOCAL_BASE)
27
+ SSD_BASE = config['SSD_BASE']
28
+ rescue ConfigLoader::ConfigNotFoundError, ConfigLoader::InvalidConfigError => e
29
+ puts e.message
30
+ exit 1
31
+ end
32
+
33
+ # Light file patterns to include (everything except heavy video files)
34
+ LIGHT_FILE_PATTERNS = %w[
35
+ **/*.srt
36
+ **/*.vtt
37
+ **/*.txt
38
+ **/*.md
39
+ **/*.jpg
40
+ **/*.jpeg
41
+ **/*.png
42
+ **/*.webp
43
+ **/*.json
44
+ **/*.yml
45
+ **/*.yaml
46
+ ].freeze
47
+
48
+ # Heavy file patterns to exclude (video files)
49
+ HEAVY_FILE_PATTERNS = %w[
50
+ *.mp4
51
+ *.mov
52
+ *.avi
53
+ *.mkv
54
+ *.webm
55
+ ].freeze
56
+
57
+ def dry_run?
58
+ ARGV.include?('--dry-run')
59
+ end
60
+
61
+ def load_manifest
62
+ unless File.exist?(MANIFEST_FILE)
63
+ puts '❌ projects.json not found!'
64
+ puts ' Run: ruby video-asset-tools/bin/generate_manifest.rb'
65
+ exit 1
66
+ end
67
+
68
+ JSON.parse(File.read(MANIFEST_FILE), symbolize_names: true)
69
+ rescue JSON::ParserError => e
70
+ puts "❌ Error parsing projects.json: #{e.message}"
71
+ exit 1
72
+ end
73
+
74
+ def should_sync_project?(project)
75
+ # Only sync if project exists on SSD but NOT in local flat structure
76
+ return false unless project[:storage][:ssd][:exists]
77
+
78
+ # Skip if exists locally in flat structure
79
+ return false if project[:storage][:local][:exists] && project[:storage][:local][:structure] == 'flat'
80
+
81
+ true
82
+ end
83
+
84
+ def validate_no_flat_conflict(project_id)
85
+ flat_path = File.join(LOCAL_BASE, project_id)
86
+ Dir.exist?(flat_path)
87
+ end
88
+
89
+ def sync_project(project)
90
+ project_id = project[:id]
91
+ ssd_path = File.join(SSD_BASE, project[:storage][:ssd][:path])
92
+ range = project[:storage][:ssd][:path].split('/')[0]
93
+ local_dir = File.join(LOCAL_ARCHIVED, range, project_id)
94
+
95
+ return skip_project_result('SSD path not found') unless Dir.exist?(ssd_path)
96
+ return skip_project_result('Flat folder exists (stale manifest?)') if validate_no_flat_conflict(project_id)
97
+
98
+ prepare_local_directory(local_dir)
99
+ sync_light_files(ssd_path, local_dir)
100
+ end
101
+
102
+ def skip_project_result(reason)
103
+ { skipped: 1, files: 0, bytes: 0, reason: reason }
104
+ end
105
+
106
+ def prepare_local_directory(local_dir)
107
+ FileUtils.mkdir_p(local_dir) if !dry_run? && !Dir.exist?(local_dir)
108
+ end
109
+
110
+ def sync_light_files(ssd_path, local_dir)
111
+ stats = { files: 0, bytes: 0 }
112
+
113
+ LIGHT_FILE_PATTERNS.each do |pattern|
114
+ Dir.glob(File.join(ssd_path, pattern)).each do |source_file|
115
+ next if heavy_file?(source_file)
116
+
117
+ copy_file_stats = copy_light_file(source_file, ssd_path, local_dir)
118
+ stats[:files] += copy_file_stats[:files]
119
+ stats[:bytes] += copy_file_stats[:bytes]
120
+ end
121
+ end
122
+
123
+ stats
124
+ end
125
+
126
+ def heavy_file?(source_file)
127
+ HEAVY_FILE_PATTERNS.any? { |pattern| File.fnmatch(pattern, File.basename(source_file)) }
128
+ end
129
+
130
+ def copy_light_file(source_file, ssd_path, local_dir)
131
+ relative_path = source_file.sub("#{ssd_path}/", '')
132
+ dest_file = File.join(local_dir, relative_path)
133
+
134
+ return { files: 0, bytes: 0 } if file_already_synced?(source_file, dest_file)
135
+
136
+ file_size = File.size(source_file)
137
+ perform_file_copy(source_file, dest_file, relative_path, file_size)
138
+
139
+ { files: 1, bytes: file_size }
140
+ end
141
+
142
+ def file_already_synced?(source_file, dest_file)
143
+ File.exist?(dest_file) && File.size(dest_file) == File.size(source_file)
144
+ end
145
+
146
+ def perform_file_copy(source_file, dest_file, relative_path, file_size)
147
+ if dry_run?
148
+ puts " [DRY-RUN] Would copy: #{relative_path} (#{format_bytes(file_size)})"
149
+ else
150
+ FileUtils.mkdir_p(File.dirname(dest_file))
151
+ FileUtils.cp(source_file, dest_file, preserve: true)
152
+ puts " ✓ Copied: #{relative_path} (#{format_bytes(file_size)})"
153
+ end
154
+ end
155
+
156
+ def format_bytes(bytes)
157
+ if bytes < 1024
158
+ "#{bytes}B"
159
+ elsif bytes < 1024 * 1024
160
+ "#{(bytes / 1024.0).round(1)}KB"
161
+ else
162
+ "#{(bytes / 1024.0 / 1024.0).round(1)}MB"
163
+ end
164
+ end
165
+
166
+ # Main execution
167
+ puts dry_run? ? '🔍 DRY-RUN MODE - No files will be copied' : '📦 Syncing from SSD...'
168
+ puts
169
+
170
+ unless Dir.exist?(SSD_BASE)
171
+ puts "❌ SSD not mounted at #{SSD_BASE}"
172
+ exit 1
173
+ end
174
+
175
+ # Load manifest
176
+ manifest = load_manifest
177
+ puts "📋 Loaded manifest: #{manifest[:projects].size} projects"
178
+ puts " Last updated: #{manifest[:config][:last_updated]}"
179
+ puts
180
+
181
+ # Filter projects to sync
182
+ projects_to_sync = manifest[:projects].select { |p| should_sync_project?(p) }
183
+
184
+ puts '🔍 Analysis:'
185
+ puts " Total projects in manifest: #{manifest[:projects].size}"
186
+ puts " Projects to sync: #{projects_to_sync.size}"
187
+ puts " Skipped (in flat structure): #{manifest[:projects].size - projects_to_sync.size}"
188
+ puts
189
+
190
+ if projects_to_sync.empty?
191
+ puts '✅ Nothing to sync - all projects either in flat structure or already synced'
192
+ exit 0
193
+ end
194
+
195
+ total_stats = { files: 0, bytes: 0, skipped: 0, validation_skipped: 0 }
196
+
197
+ projects_to_sync.each do |project|
198
+ stats = sync_project(project)
199
+
200
+ # Only show project if there are files to sync or a warning
201
+ if stats[:reason] || stats[:files]&.positive?
202
+ puts "📁 #{project[:id]}"
203
+
204
+ if stats[:reason]
205
+ puts " ⚠️ Skipped: #{stats[:reason]}"
206
+ total_stats[:validation_skipped] += 1 if stats[:reason].include?('stale')
207
+ end
208
+
209
+ puts " #{stats[:files]} file(s), #{format_bytes(stats[:bytes])}" if stats[:files]&.positive?
210
+ puts
211
+ end
212
+
213
+ total_stats[:files] += stats[:files] || 0
214
+ total_stats[:bytes] += stats[:bytes] || 0
215
+ total_stats[:skipped] += stats[:skipped] || 0
216
+ end
217
+
218
+ puts
219
+ puts '=' * 60
220
+ puts 'Summary:'
221
+ puts " Projects scanned: #{projects_to_sync.size}"
222
+ puts " Projects skipped (validation): #{total_stats[:validation_skipped]}" if total_stats[:validation_skipped].positive?
223
+ puts " Files #{dry_run? ? 'to copy' : 'copied'}: #{total_stats[:files]}"
224
+ puts " Total size: #{format_bytes(total_stats[:bytes])}"
225
+ puts
226
+
227
+ if total_stats[:validation_skipped].positive?
228
+ puts '⚠️ WARNING: Some projects were skipped due to validation failures'
229
+ puts ' This may indicate a stale manifest. Consider running:'
230
+ puts ' ruby video-asset-tools/bin/generate_manifest.rb'
231
+ puts
232
+ end
233
+
234
+ puts '✅ Sync complete!'
235
+ puts ' Run without --dry-run to perform the sync' if dry_run?
236
+ puts " Run 'ruby video-asset-tools/bin/generate_manifest.rb' to update manifest with new state" unless dry_run?