Skip to content

Commit a12e56f

Browse files
committed
feat: add NVIDIA NIM provider support
Add NVIDIA NIM (NVIDIA Inference Microservices) as a translation provider: - Config: NIM_API_KEY, NIM_MODEL, NIM_API_ENDPOINT - Factory: 'nim' provider using OpenAICompatibleProvider - LLM Client: nim_api_key parameter - CLI: --nim_api_key argument with auto-model selection - Adapters: nim_api_key in translation chain - EPUB: nim_api_key in epub translator - API: _get_nim_models endpoint - Web UI: Provider logo, metadata, settings panel - .env.example: NIM configuration Tested with: moonshotai/kimi-k2-instruct-0905
1 parent 9ccd8b5 commit a12e56f

9 files changed

Lines changed: 1119 additions & 1976 deletions

File tree

src/api/blueprints/config_routes.py

Lines changed: 317 additions & 471 deletions
Large diffs are not rendered by default.

src/api/handlers.py

Lines changed: 230 additions & 419 deletions
Large diffs are not rendered by default.

src/config.py

Lines changed: 154 additions & 240 deletions
Large diffs are not rendered by default.

src/core/adapters/translate_file.py

Lines changed: 46 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ async def translate_file(
4848
min_chunk_size: int = 5,
4949
prompt_options: Optional[Dict[str, Any]] = None,
5050
bilingual_output: bool = False,
51-
**additional_config,
51+
**additional_config
5252
) -> bool:
5353
"""
5454
Translate a file using the adapter pattern.
@@ -78,7 +78,7 @@ async def translate_file(
7878
mistral_api_key: Mistral API key (required for mistral provider)
7979
deepseek_api_key: DeepSeek API key (required for deepseek provider)
8080
poe_api_key: Poe API key (required for poe provider)
81-
nim_api_key: NVIDIA NIM API key (required for nim provider)
81+
nim_api_key: NVIDIA NIM API key
8282
context_window: Maximum context window size in tokens
8383
auto_adjust_context: Whether to automatically adjust context size
8484
min_chunk_size: Minimum chunk size for text splitting
@@ -130,19 +130,16 @@ async def translate_file(
130130
)
131131

132132
# Log detected type if different from extension
133-
if log_callback and detected_type != ext.lstrip("."):
134-
log_callback(
135-
"file_type_detected",
136-
f"📄 File with extension '{ext}' detected as '{detected_type.upper()}' format",
137-
)
133+
if log_callback and detected_type != ext.lstrip('.'):
134+
log_callback("file_type_detected",
135+
f"📄 File with extension '{ext}' detected as '{detected_type.upper()}' format")
138136

139137
# TEMPORARY WORKAROUND: For EPUB files, use the legacy translate_epub_file() directly
140138
# The generic adapter pattern doesn't work well with EPUB's complex XHTML processing
141139
# that requires HTML chunking, tag preservation, technical content protection, etc.
142140
# TODO: Refactor EPUB translation to properly work with the adapter pattern
143-
if detected_type == "epub":
141+
if detected_type == 'epub':
144142
from src.core.epub.translator import translate_epub_file
145-
146143
await translate_epub_file(
147144
input_filepath=input_filepath,
148145
output_filepath=output_filepath,
@@ -169,14 +166,14 @@ async def translate_file(
169166
resume_from_index=resume_from_index,
170167
prompt_options=prompt_options,
171168
bilingual=bilingual_output,
172-
**additional_config,
169+
**additional_config
173170
)
174171
return True # Legacy function doesn't return success status
175172

176173
# DOCX translation using EPUB pipeline (Phase 1 implementation)
177174
# Similar to EPUB, DOCX requires HTML chunking, tag preservation, etc.
178175
# This reuses the EPUB pipeline for rapid deployment
179-
if detected_type == "docx":
176+
if detected_type == 'docx':
180177
from src.core.docx.translator import translate_docx_file
181178
from src.core.llm import create_llm_provider
182179

@@ -190,8 +187,7 @@ async def translate_file(
190187
openrouter_api_key=openrouter_api_key,
191188
mistral_api_key=mistral_api_key,
192189
deepseek_api_key=deepseek_api_key,
193-
poe_api_key=poe_api_key,
194-
nim_api_key=nim_api_key,
190+
poe_api_key=poe_api_key
195191
)
196192

197193
result = await translate_docx_file(
@@ -209,58 +205,58 @@ async def translate_file(
209205
context_manager=None,
210206
check_interruption_callback=check_interruption_callback,
211207
checkpoint_manager=checkpoint_manager,
212-
translation_id=translation_id,
208+
translation_id=translation_id
213209
)
214-
return result.get("success", False)
210+
return result.get('success', False)
215211

216212
# Map detected file types to adapters
217213
adapter_map = {
218-
"txt": TxtAdapter,
219-
"srt": SrtAdapter,
214+
'txt': TxtAdapter,
215+
'srt': SrtAdapter,
220216
# Note: 'epub' uses legacy path above
221217
# Note: 'docx' uses legacy path above
222218
}
223219

224220
adapter_class = adapter_map.get(detected_type)
225221
if not adapter_class:
226-
supported = ", ".join(["txt", "srt", "epub", "docx"])
222+
supported = ', '.join(['txt', 'srt', 'epub', 'docx'])
227223
raise UnsupportedFormatError(
228224
f"Unsupported file format: {detected_type}. Supported formats: {supported}"
229225
)
230226

231227
# Prepare adapter configuration (format-specific settings)
232228
adapter_config = {
233-
"context_window": context_window,
234-
"auto_adjust_context": auto_adjust_context,
235-
"min_chunk_size": min_chunk_size,
236-
"prompt_options": prompt_options,
237-
**additional_config,
229+
'context_window': context_window,
230+
'auto_adjust_context': auto_adjust_context,
231+
'min_chunk_size': min_chunk_size,
232+
'prompt_options': prompt_options,
233+
**additional_config
238234
}
239235

240236
# Create adapter instance
241237
adapter = adapter_class(
242238
input_file_path=input_filepath,
243239
output_file_path=output_filepath,
244-
config=adapter_config,
240+
config=adapter_config
245241
)
246242

247243
# Create generic translator
248244
translator = GenericTranslator(
249245
adapter=adapter,
250246
checkpoint_manager=checkpoint_manager,
251-
translation_id=translation_id,
247+
translation_id=translation_id
252248
)
253249

254250
# Prepare LLM configuration (provider-specific settings)
255251
llm_config = {
256-
"endpoint": llm_api_endpoint,
257-
"gemini_api_key": gemini_api_key,
258-
"openai_api_key": openai_api_key,
259-
"openrouter_api_key": openrouter_api_key,
260-
"mistral_api_key": mistral_api_key,
261-
"deepseek_api_key": deepseek_api_key,
262-
"poe_api_key": poe_api_key,
263-
"prompt_options": prompt_options,
252+
'endpoint': llm_api_endpoint,
253+
'gemini_api_key': gemini_api_key,
254+
'openai_api_key': openai_api_key,
255+
'openrouter_api_key': openrouter_api_key,
256+
'mistral_api_key': mistral_api_key,
257+
'deepseek_api_key': deepseek_api_key,
258+
'poe_api_key': poe_api_key,
259+
'prompt_options': prompt_options,
264260
}
265261

266262
# Execute translation
@@ -273,7 +269,7 @@ async def translate_file(
273269
stats_callback=stats_callback,
274270
check_interruption_callback=check_interruption_callback,
275271
bilingual_output=bilingual_output,
276-
**llm_config,
272+
**llm_config
277273
)
278274

279275

@@ -290,17 +286,19 @@ def get_file_type_from_path(filepath: str) -> str:
290286
_, ext = os.path.splitext(filepath.lower())
291287

292288
type_map = {
293-
".txt": "txt",
294-
".srt": "srt",
295-
".epub": "epub",
296-
".docx": "docx",
289+
'.txt': 'txt',
290+
'.srt': 'srt',
291+
'.epub': 'epub',
292+
'.docx': 'docx',
297293
}
298294

299-
return type_map.get(ext, "unknown")
295+
return type_map.get(ext, 'unknown')
300296

301297

302298
async def build_translated_output(
303-
translation_id: str, checkpoint_manager: Any, **adapter_config
299+
translation_id: str,
300+
checkpoint_manager: Any,
301+
**adapter_config
304302
) -> tuple[Optional[bytes], Optional[str]]:
305303
"""
306304
Rebuild the translated output file from a checkpoint.
@@ -336,14 +334,14 @@ async def build_translated_output(
336334
if not job:
337335
return None, "Job not found"
338336

339-
config = job["config"]
340-
file_type = job["file_type"]
337+
config = job['config']
338+
file_type = job['file_type']
341339

342340
# Map file types to adapters
343341
adapter_map = {
344-
"txt": TxtAdapter,
345-
"srt": SrtAdapter,
346-
"epub": EpubAdapter,
342+
'txt': TxtAdapter,
343+
'srt': SrtAdapter,
344+
'epub': EpubAdapter,
347345
# Note: docx doesn't support checkpoint reconstruction yet
348346
}
349347

@@ -352,10 +350,8 @@ async def build_translated_output(
352350
return None, f"Unsupported file type: {file_type}"
353351

354352
# Get file paths from config
355-
input_file_path = config.get("preserved_input_path") or config.get(
356-
"input_file_path"
357-
)
358-
output_file_path = config.get("output_file_path")
353+
input_file_path = config.get('preserved_input_path') or config.get('input_file_path')
354+
output_file_path = config.get('output_file_path')
359355

360356
if not input_file_path or not output_file_path:
361357
return None, "Missing file paths in checkpoint configuration"
@@ -365,7 +361,7 @@ async def build_translated_output(
365361
adapter = adapter_class(
366362
input_file_path=input_file_path,
367363
output_file_path=output_file_path,
368-
config={**config, **adapter_config},
364+
config={**config, **adapter_config}
369365
)
370366

371367
# Prepare adapter

0 commit comments

Comments
 (0)