llm_translate 0.2.0 → 0.4.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: c1231cf03fe5984a00c4bccf9596b2255dd83c4852bac5bd554068e3853f1fad
4
- data.tar.gz: 208488f548d281ae103eb10ad3790201c41a62cff8acb0b24c36e786181da3ed
3
+ metadata.gz: 9ab1da1df23759b64860a8f6c5e33e022d8a73aa842ed494448cb121a55ecaf8
4
+ data.tar.gz: 1f3536587e0a55c4c09720584cb798538084953a1dd0bec004517f7a28eb46e6
5
5
  SHA512:
6
- metadata.gz: bc4713b2292f56da626f79beafda11144a45f8fc84ed660f4f333afa2cf93b592285bb74b5746cb2610cf5bf41c81006ffb8ad648c6784365692404e4ff56495
7
- data.tar.gz: 0f618ca45a6b97cf3fed6410d2dcf73bd80fd17abc0e91c443110021da5da0b484945b45f9813ef46aa671714c821ad1aa73f51530165cc9eceea437b32ed3e3
6
+ metadata.gz: 95e9bd1e7166440b416dc9b1bc92fb6ca3b2a4f96ce9460973753cdae24ed7868cca9fd270c05a0316e3c33c96bbef9000f704a71e0f9564815e6b597e103d35
7
+ data.tar.gz: 262162b0d498b1673a1cebc5ca7ea1fcd97924ee5019fd7b000e7c8a089ac97d87e57ec0686d0cc84d755b5f3500218449f273be35cfc34ecc9b5057902691d5
data/README.zh.md CHANGED
@@ -1,17 +1,17 @@
1
1
  # LlmTranslate
2
2
 
3
- AI 驱动的 Markdown 翻译工具,可在使用各种 AI 提供商翻译内容时保留格式。
3
+ AI 驱动的 Markdown 翻译工具,在使用各种 AI 提供商翻译内容的同时保持格式不变。
4
4
 
5
5
  ## 功能特性
6
6
 
7
7
  - 🤖 **AI 驱动翻译**:支持 OpenAI、Anthropic 和 Ollama
8
- - 📝 **Markdown 格式保留**:保持代码块、链接、图片和格式完整
8
+ - 📝 **Markdown 格式保留**:保持代码块、链接、图片和格式不变
9
9
  - 🔧 **灵活配置**:基于 YAML 的配置,支持环境变量
10
10
  - 📁 **批量处理**:递归处理整个目录结构
11
- - 🚀 **CLI 界面**:使用 Thor 的易用命令行界面
11
+ - 🚀 **CLI 界面**:使用 Thor 实现的易用命令行界面
12
12
  - 📊 **进度跟踪**:内置日志记录和报告功能
13
- - ⚡ **错误处理**:带有重试机制的强大错误处理
14
- - 🎯 **可定制**:自定义提示、文件模式和输出策略
13
+ - ⚡ **错误处理**:具有重试机制的强大错误处理
14
+ - 🎯 **可定制性**:自定义提示、文件模式和输出策略
15
15
 
16
16
  ## 安装
17
17
 
@@ -149,7 +149,7 @@ Options:
149
149
  -o, --output PATH 输出目录(覆盖配置)
150
150
  -p, --prompt TEXT 自定义翻译提示(覆盖配置)
151
151
  -v, --verbose 启用详细输出
152
- -d, --dry-run 执行试运行,不进行实际翻译
152
+ -d, --dry-run 执行试运行而不进行实际翻译
153
153
 
154
154
  Other Commands:
155
155
  llm_translate init 初始化新的配置文件
@@ -70,24 +70,19 @@ module LlmTranslate
70
70
  end
71
71
 
72
72
  # Translate files
73
- success_count = 0
74
- error_count = 0
75
-
76
- files.each_with_index do |file_path, index|
77
- logger.info "[#{index + 1}/#{files.length}] Processing: #{file_path}"
78
-
79
- translator_engine.translate_file(file_path) unless options[:dry_run]
73
+ if options[:dry_run]
74
+ logger.info "DRY RUN: Would translate #{files.length} files with #{config.concurrent_files} concurrent threads"
75
+ success_count = files.length
76
+ error_count = 0
77
+ else
78
+ logger.info "Starting translation with #{config.concurrent_files} concurrent files"
80
79
 
81
- success_count += 1
82
- logger.info "✓ Successfully processed: #{file_path}"
83
- rescue StandardError => e
84
- error_count += 1
85
- logger.error "✗ Failed to process #{file_path}: #{e.message}"
80
+ results = translator_engine.translate_files_concurrently(files)
81
+ success_count = results[:success].length
82
+ error_count = results[:error].length
86
83
 
87
- if config.should_stop_on_error?(error_count)
88
- logger.error 'Stopping due to too many consecutive errors'
89
- break
90
- end
84
+ # Check if we should stop on too many errors
85
+ logger.error "Stopping due to too many errors (#{error_count})" if config.should_stop_on_error?(error_count)
91
86
  end
92
87
 
93
88
  # Summary
@@ -39,7 +39,7 @@ module LlmTranslate
39
39
  end
40
40
 
41
41
  def max_tokens
42
- data.dig('ai', 'max_tokens') || 4000
42
+ data.dig('ai', 'max_tokens') || 40_000
43
43
  end
44
44
 
45
45
  def retry_attempts
@@ -85,15 +85,24 @@ module LlmTranslate
85
85
  end
86
86
 
87
87
  def input_file
88
+ return cli_options[:input] if cli_options[:input]
89
+
88
90
  data.dig('files', 'input_file')
89
91
  end
90
92
 
91
93
  def output_file
94
+ return cli_options[:output] if cli_options[:input] && cli_options[:output]
95
+
92
96
  data.dig('files', 'output_file')
93
97
  end
94
98
 
95
99
  def single_file_mode?
96
- !input_file.nil? && !output_file.nil?
100
+ input_file_path = input_file
101
+ output_file_path = output_file
102
+
103
+ # Both must be present and input must be a file (not directory) for single file mode
104
+ !input_file_path.nil? && !output_file_path.nil? &&
105
+ File.exist?(input_file_path) && File.file?(input_file_path)
97
106
  end
98
107
 
99
108
  def filename_strategy
@@ -120,10 +129,6 @@ module LlmTranslate
120
129
  data.dig('files', 'overwrite_policy') || 'ask'
121
130
  end
122
131
 
123
- def backup_directory
124
- data.dig('files', 'backup_directory') || './backups'
125
- end
126
-
127
132
  # Logging Configuration
128
133
  def log_level
129
134
  cli_options[:verbose] ? 'debug' : (data.dig('logging', 'level') || 'info')
@@ -133,18 +138,10 @@ module LlmTranslate
133
138
  data.dig('logging', 'output') || 'console'
134
139
  end
135
140
 
136
- def log_file_path
137
- data.dig('logging', 'file_path') || './logs/llm_translate.log'
138
- end
139
-
140
141
  def verbose_translation?
141
142
  cli_options[:verbose] || data.dig('logging', 'verbose_translation') == true
142
143
  end
143
144
 
144
- def error_log_path
145
- data.dig('logging', 'error_log_path') || './logs/errors.log'
146
- end
147
-
148
145
  # Error Handling Configuration
149
146
  def on_error
150
147
  data.dig('error_handling', 'on_error') || 'log_and_continue'
@@ -162,10 +159,6 @@ module LlmTranslate
162
159
  data.dig('error_handling', 'generate_error_report') != false
163
160
  end
164
161
 
165
- def error_report_path
166
- data.dig('error_handling', 'error_report_path') || './logs/error_report.md'
167
- end
168
-
169
162
  def should_stop_on_error?(error_count)
170
163
  on_error == 'stop' || error_count >= max_consecutive_errors
171
164
  end
@@ -175,27 +168,11 @@ module LlmTranslate
175
168
  data.dig('performance', 'concurrent_files') || 3
176
169
  end
177
170
 
178
- def batch_size
179
- data.dig('performance', 'batch_size') || 5
180
- end
181
-
182
171
  def request_interval
183
172
  data.dig('performance', 'request_interval') || 1
184
173
  end
185
174
 
186
- def max_memory_mb
187
- data.dig('performance', 'max_memory_mb') || 500
188
- end
189
-
190
175
  # Output Configuration
191
- def show_progress?
192
- data.dig('output', 'show_progress') != false
193
- end
194
-
195
- def show_statistics?
196
- data.dig('output', 'show_statistics') != false
197
- end
198
-
199
176
  def generate_report?
200
177
  data.dig('output', 'generate_report') != false
201
178
  end
@@ -204,14 +181,6 @@ module LlmTranslate
204
181
  data.dig('output', 'report_path') || './reports/translation_report.md'
205
182
  end
206
183
 
207
- def output_format
208
- data.dig('output', 'format') || 'markdown'
209
- end
210
-
211
- def include_metadata?
212
- data.dig('output', 'include_metadata') != false
213
- end
214
-
215
184
  private
216
185
 
217
186
  def load_config_file(config_path)
@@ -236,9 +205,52 @@ module LlmTranslate
236
205
  'API key is required. Set LLM_TRANSLATE_API_KEY environment variable or configure in config file.'
237
206
  end
238
207
 
239
- return if Dir.exist?(File.dirname(input_directory))
208
+ # Validate input/output based on mode
209
+ if single_file_mode?
210
+ validate_single_file_mode
211
+ else
212
+ validate_directory_mode
213
+ end
214
+ end
215
+
216
+ def validate_single_file_mode
217
+ # Validate input file exists
218
+ unless input_file && File.exist?(input_file)
219
+ raise ConfigurationError, "Input file does not exist: #{input_file || 'not specified'}"
220
+ end
221
+
222
+ # Validate input is actually a file
223
+ unless File.file?(input_file)
224
+ raise ConfigurationError, "Input path is not a file: #{input_file}"
225
+ end
226
+
227
+ # Validate output file path
228
+ unless output_file
229
+ raise ConfigurationError, "Output file must be specified for single file mode"
230
+ end
240
231
 
241
- raise ConfigurationError, "Input directory parent does not exist: #{File.dirname(input_directory)}"
232
+ # Ensure output directory exists
233
+ output_dir = File.dirname(output_file)
234
+ unless Dir.exist?(output_dir)
235
+ begin
236
+ FileUtils.mkdir_p(output_dir)
237
+ rescue StandardError => e
238
+ raise ConfigurationError, "Cannot create output directory #{output_dir}: #{e.message}"
239
+ end
240
+ end
241
+ end
242
+
243
+ def validate_directory_mode
244
+ # Validate input directory
245
+ unless Dir.exist?(input_directory)
246
+ raise ConfigurationError, "Input directory does not exist: #{input_directory}"
247
+ end
248
+
249
+ # Ensure output directory parent exists
250
+ output_parent = File.dirname(output_directory)
251
+ unless Dir.exist?(output_parent)
252
+ raise ConfigurationError, "Output directory parent does not exist: #{output_parent}"
253
+ end
242
254
  end
243
255
 
244
256
  def resolve_env_var(value)
@@ -67,7 +67,7 @@ module LlmTranslate
67
67
  when 'console'
68
68
  create_console_logger
69
69
  when 'file'
70
- create_file_logger(config.log_file_path)
70
+ create_file_logger('./logs/llm_translate.log')
71
71
  when 'both'
72
72
  create_multi_logger
73
73
  else
@@ -76,15 +76,8 @@ module LlmTranslate
76
76
  end
77
77
 
78
78
  def create_error_logger
79
- return nil unless config.error_log_path
80
-
81
- FileUtils.mkdir_p(File.dirname(config.error_log_path))
82
- error_logger = ::Logger.new(config.error_log_path)
83
- error_logger.level = log_level_constant
84
- error_logger.formatter = proc do |severity, datetime, _progname, msg|
85
- "[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity}: #{msg}\n"
86
- end
87
- error_logger
79
+ # Error logger is no longer supported, return nil
80
+ nil
88
81
  end
89
82
 
90
83
  def create_console_logger
@@ -119,7 +112,7 @@ module LlmTranslate
119
112
 
120
113
  def create_multi_logger
121
114
  console_logger = create_console_logger
122
- file_logger = create_file_logger(config.log_file_path)
115
+ file_logger = create_file_logger('./logs/llm_translate.log')
123
116
 
124
117
  MultiLogger.new([console_logger, file_logger])
125
118
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'pathname'
4
4
  require 'fileutils'
5
+ require 'async'
5
6
 
6
7
  module LlmTranslate
7
8
  class TranslatorEngine
@@ -53,6 +54,66 @@ module LlmTranslate
53
54
  sleep(config.request_interval) if config.request_interval.positive?
54
55
  end
55
56
 
57
+ def translate_files_concurrently(file_paths)
58
+ return translate_files_sequentially(file_paths) if config.concurrent_files <= 1
59
+
60
+ results = { success: [], error: [] }
61
+
62
+ # Use Async to run concurrent translation tasks
63
+ Async do |task|
64
+ # Process files in batches to limit concurrency
65
+ file_paths.each_slice(config.concurrent_files) do |batch|
66
+ # Create async tasks for the current batch
67
+ batch_tasks = batch.map.with_index do |file_path, _batch_index|
68
+ # Calculate overall index
69
+ overall_index = file_paths.index(file_path) + 1
70
+
71
+ task.async do
72
+ logger.info "[#{overall_index}/#{file_paths.length}] Processing: #{file_path}"
73
+
74
+ # Translate the file
75
+ translate_file(file_path)
76
+
77
+ # Collect successful result
78
+ results[:success] << file_path
79
+
80
+ logger.info "✓ Successfully processed: #{file_path}"
81
+ { status: :success, file: file_path }
82
+ rescue StandardError => e
83
+ # Collect error result
84
+ results[:error] << { file: file_path, error: e.message }
85
+
86
+ logger.error "✗ Failed to process #{file_path}: #{e.message}"
87
+ { status: :error, file: file_path, error: e.message }
88
+ end
89
+ end
90
+
91
+ # Wait for all tasks in this batch to complete before starting the next batch
92
+ batch_tasks.each(&:wait)
93
+ end
94
+ end
95
+
96
+ results
97
+ end
98
+
99
+ def translate_files_sequentially(file_paths)
100
+ results = { success: [], error: [] }
101
+
102
+ file_paths.each_with_index do |file_path, index|
103
+ logger.info "[#{index + 1}/#{file_paths.length}] Processing: #{file_path}"
104
+
105
+ translate_file(file_path)
106
+
107
+ results[:success] << file_path
108
+ logger.info "✓ Successfully processed: #{file_path}"
109
+ rescue StandardError => e
110
+ results[:error] << { file: file_path, error: e.message }
111
+ logger.error "✗ Failed to process #{file_path}: #{e.message}"
112
+ end
113
+
114
+ results
115
+ end
116
+
56
117
  def translate_content(content, file_path = nil)
57
118
  if config.preserve_formatting?
58
119
  translate_with_format_preservation(content)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LlmTranslate
4
- VERSION = '0.2.0'
4
+ VERSION = '0.4.0'
5
5
  end
@@ -31,6 +31,7 @@ Gem::Specification.new do |spec|
31
31
  spec.require_paths = ['lib']
32
32
 
33
33
  # Dependencies
34
+ spec.add_dependency 'async', '~> 2.0'
34
35
  spec.add_dependency 'ruby_llm', '~> 1.6'
35
36
  spec.add_dependency 'thor', '~> 1.3'
36
37
 
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm_translate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - LlmTranslate Team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-08-19 00:00:00.000000000 Z
11
+ date: 2025-09-01 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: async
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: ruby_llm
15
29
  requirement: !ruby/object:Gem::Requirement