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 +4 -4
- data/README.zh.md +6 -6
- data/lib/llm_translate/cli.rb +11 -16
- data/lib/llm_translate/config.rb +56 -44
- data/lib/llm_translate/logger.rb +4 -11
- data/lib/llm_translate/translator_engine.rb +61 -0
- data/lib/llm_translate/version.rb +1 -1
- data/llm_translate.gemspec +1 -0
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9ab1da1df23759b64860a8f6c5e33e022d8a73aa842ed494448cb121a55ecaf8
|
4
|
+
data.tar.gz: 1f3536587e0a55c4c09720584cb798538084953a1dd0bec004517f7a28eb46e6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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 初始化新的配置文件
|
data/lib/llm_translate/cli.rb
CHANGED
@@ -70,24 +70,19 @@ module LlmTranslate
|
|
70
70
|
end
|
71
71
|
|
72
72
|
# Translate files
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
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
|
88
|
-
|
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
|
data/lib/llm_translate/config.rb
CHANGED
@@ -39,7 +39,7 @@ module LlmTranslate
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def max_tokens
|
42
|
-
data.dig('ai', 'max_tokens') ||
|
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
|
-
|
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
|
-
|
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
|
-
|
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)
|
data/lib/llm_translate/logger.rb
CHANGED
@@ -67,7 +67,7 @@ module LlmTranslate
|
|
67
67
|
when 'console'
|
68
68
|
create_console_logger
|
69
69
|
when 'file'
|
70
|
-
create_file_logger(
|
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
|
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(
|
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)
|
data/llm_translate.gemspec
CHANGED
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.
|
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-
|
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
|