davideuler commited on
Commit
57c2b26
·
1 Parent(s): 79d1413

记录调用语言模型的错误日志;PDF文件可以下载,优化下载的PDF文件大小

Browse files
.gitignore CHANGED
@@ -23,6 +23,8 @@ var/
23
  .idea/
24
  .idea
25
  .cached
 
 
26
  wheels/
27
  *.egg-info/
28
  .installed.cfg
 
23
  .idea/
24
  .idea
25
  .cached
26
+ *.pdf
27
+ *.log
28
  wheels/
29
  *.egg-info/
30
  .installed.cfg
README.md CHANGED
@@ -73,6 +73,25 @@ cmake --build build --config Release -j 12
73
 
74
  ```
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  ### Options 3. Local inference service by ollama/vLLM and other application such as LMStudio
77
 
78
  Please read the official guide for you LLM inferencing tool.
 
73
 
74
  ```
75
 
76
+ ## Note on using OpenAI Compatible LLM service provider
77
+
78
+ For example, run the following command before start the streamlit application to enable translation by deepseek :
79
+
80
+ ``` bash
81
+ export OPENAI_MODEL=deepseek-chat
82
+ export OPENAI_API_BASE=https://api.deepseek.com/v1
83
+ export OPENAI_API_KEY=sk-xxxx
84
+ ```
85
+
86
+ Run the following command before start the streamlit application to enable translation by moonshot :
87
+
88
+ ``` bash
89
+ export OPENAI_MODEL=moonshot-v1-8k
90
+ export OPENAI_API_BASE=https://api.moonshot.cn/v1
91
+ export OPENAI_API_KEY=sk-xxxx
92
+ ```
93
+
94
+
95
  ### Options 3. Local inference service by ollama/vLLM and other application such as LMStudio
96
 
97
  Please read the official guide for you LLM inferencing tool.
deep_translator/openai_compatible.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import time
3
+ import os,logging
4
+
5
+ import streamlit as st
6
+ from .chatgpt import ChatGptTranslator
7
+
8
+ logging.basicConfig(filename='application.log', level=logging.INFO, format='%(asctime)s - %(levelname)-5s %(lineno)d %(filename)s:%(funcName)s - %(message)s')
9
+
10
+ class OpenAICompatibleTranslator(ChatGptTranslator):
11
+ """Translator that handles OpenAI compatible APIs with better error handling"""
12
+ def __init__(self, source="en", target="zh-CN", **kwargs):
13
+ super().__init__(source=source, target=target, **kwargs)
14
+ self.retry_count = 3
15
+ self.retry_delay = 1 # seconds
16
+
17
+ def translate(self, text: str, **kwargs) -> str:
18
+ """
19
+ Translate text with retry mechanism and error handling
20
+ """
21
+ if not text.strip():
22
+ return text
23
+
24
+ for attempt in range(self.retry_count):
25
+ try:
26
+ api_base = os.environ["OPENAI_API_BASE"]
27
+ logging.info(f"Request OpenAI compatible api, base_url: {api_base}")
28
+ return super().translate(text, **kwargs)
29
+ except json.JSONDecodeError:
30
+ logging.warn(f"Translation API response JSONDecodeError, will retry later...")
31
+ if attempt == self.retry_count - 1:
32
+ logging.error(f"Translation API response error, using original text")
33
+ st.warning(f"Translation API response error, using original text")
34
+ return text
35
+ time.sleep(self.retry_delay)
36
+ except Exception as e:
37
+ logging.error(f"Translation error: {str(e)}")
38
+ st.error(f"Translation error: {str(e)}")
39
+ return text
pdf_translator_web.py CHANGED
@@ -6,8 +6,9 @@ import streamlit as st
6
  import pymupdf
7
  from deep_translator import (
8
  GoogleTranslator,
9
- ChatGptTranslator,
10
  )
 
 
11
 
12
  # Constants
13
  DEFAULT_PAGES_PER_LOAD = 2
@@ -16,8 +17,9 @@ DEFAULT_API_BASE = "http://localhost:8080/v1"
16
 
17
  # Supported translators
18
  TRANSLATORS = {
 
 
19
  'google': GoogleTranslator,
20
- 'chatgpt': ChatGptTranslator,
21
  }
22
 
23
  # Color options
@@ -60,23 +62,29 @@ def get_cache_dir():
60
  cache_dir.mkdir(exist_ok=True)
61
  return cache_dir
62
 
63
- def get_cache_key(file_content: bytes, page_num: int, translator_name: str, target_lang: str):
64
  """Generate cache key for a specific page translation"""
65
- # 使用文件内容的hash作为缓存key的一部分
66
- file_hash = hashlib.md5(file_content).hexdigest()
67
- return f"{file_hash}_page{page_num}_{translator_name}_{target_lang}.pdf"
 
 
68
 
69
  def get_cached_translation(cache_key: str) -> pymupdf.Document:
70
  """Get cached translation if exists"""
71
  cache_path = get_cache_dir() / cache_key
72
  if cache_path.exists():
73
- return pymupdf.open(str(cache_path))
 
 
 
 
74
  return None
75
 
76
  def save_translation_cache(doc: pymupdf.Document, cache_key: str):
77
  """Save translation to cache"""
78
  cache_path = get_cache_dir() / cache_key
79
- doc.save(str(cache_path))
80
 
81
  def translate_pdf_pages(doc, doc_bytes, start_page, num_pages, translator, text_color, translator_name, target_lang):
82
  """Translate specific pages of a PDF document with progress and caching"""
@@ -84,6 +92,8 @@ def translate_pdf_pages(doc, doc_bytes, start_page, num_pages, translator, text_
84
  rgb_color = COLOR_MAP.get(text_color.lower(), COLOR_MAP["darkred"])
85
 
86
  translated_pages = []
 
 
87
 
88
  # Create a progress bar
89
  progress_bar = st.progress(0)
@@ -92,13 +102,30 @@ def translate_pdf_pages(doc, doc_bytes, start_page, num_pages, translator, text_
92
  for i, page_num in enumerate(range(start_page, min(start_page + num_pages, doc.page_count))):
93
  status_text.text(f"Translating page {page_num + 1}...")
94
 
95
- # Check cache first
96
- cache_key = get_cache_key(doc_bytes, page_num, translator_name, target_lang)
 
 
 
 
 
 
 
 
 
 
 
97
  cached_doc = get_cached_translation(cache_key)
98
 
99
  if cached_doc is not None:
100
  translated_pages.append(cached_doc)
 
 
 
101
  else:
 
 
 
102
  # Create a new PDF document for this page
103
  new_doc = pymupdf.open()
104
  new_doc.insert_pdf(doc, from_page=page_num, to_page=page_num)
@@ -110,8 +137,6 @@ def translate_pdf_pages(doc, doc_bytes, start_page, num_pages, translator, text_
110
  for block in blocks:
111
  bbox = block[:4]
112
  text = block[4]
113
-
114
- # Translate the text
115
  translated = translator.translate(text)
116
 
117
  # Cover original text with white and add translation in color
@@ -125,28 +150,80 @@ def translate_pdf_pages(doc, doc_bytes, start_page, num_pages, translator, text_
125
  # Save to cache
126
  save_translation_cache(new_doc, cache_key)
127
  translated_pages.append(new_doc)
 
128
 
129
  # Update progress
130
- progress = (i + 1) / min(num_pages, doc.page_count - start_page)
131
  progress_bar.progress(progress)
132
 
133
- # Clear progress indicators
134
  progress_bar.empty()
135
- status_text.empty()
 
136
 
137
  return translated_pages
138
 
139
- def get_page_image(page, scale=2.0):
140
  """Get high quality image from PDF page"""
141
  # 计算缩放后的尺寸
142
  zoom = scale
143
  mat = pymupdf.Matrix(zoom, zoom)
144
 
145
- # 使用高分辨率渲染页面
146
- pix = page.get_pixmap(matrix=mat, alpha=False)
 
 
 
 
147
 
148
  return pix
149
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  def main():
151
  st.set_page_config(layout="wide", page_title="PDF Translator for Human: with Local-LLM/GPT")
152
  st.title("PDF Translator for Human: with Local-LLM/GPT")
@@ -184,35 +261,44 @@ def main():
184
  index=0
185
  )
186
 
187
- # ChatGPT specific settings
188
- if translator_name == 'chatgpt':
189
- st.subheader("ChatGPT Settings")
190
  target_lang = st.selectbox(
191
  "Target Language",
192
  options=list(LANGUAGE_OPTIONS.keys()),
193
  index=0
194
  )
195
- api_key = st.text_input(
196
- "OpenAI API Key",
197
- value=os.getenv("OPENAI_API_KEY", ""),
198
- type="password"
199
- )
 
 
 
 
 
 
 
200
  api_base = st.text_input(
201
  "API Base URL",
202
- value=os.getenv("OPENAI_API_BASE", DEFAULT_API_BASE)
203
  )
 
 
 
204
  model = st.text_input(
205
  "Model Name",
206
- value=os.getenv("OPENAI_MODEL", DEFAULT_MODEL)
207
  )
208
 
209
  # Update environment variables
210
- os.environ["OPENAI_API_KEY"] = api_key
211
  os.environ["OPENAI_API_BASE"] = api_base
212
  os.environ["OPENAI_MODEL"] = model
213
  target_lang = LANGUAGE_OPTIONS[target_lang]
214
  else:
215
- # For Google Translator, also show target language selection
216
  target_lang_name = st.selectbox(
217
  "Target Language",
218
  options=list(SOURCE_LANGUAGE_OPTIONS.keys())[:-1], # Remove "Auto" option
@@ -233,6 +319,12 @@ def main():
233
  st.session_state.current_page = 0
234
  st.session_state.translation_started = True # 自动开始翻译
235
 
 
 
 
 
 
 
236
  # Display original pages immediately
237
  with col1:
238
  st.header("Original")
@@ -268,22 +360,79 @@ def main():
268
  pix = get_page_image(page)
269
  st.image(pix.tobytes(), caption=f"Page {st.session_state.current_page + i + 1}", use_container_width=True)
270
 
271
- # Navigation buttons
272
- nav_col1, nav_col2 = st.columns(2)
273
- with nav_col1:
 
 
 
274
  if st.session_state.current_page > 0:
275
- if st.button("Previous Pages"):
276
  st.session_state.current_page = max(0, st.session_state.current_page - pages_per_load)
277
  st.rerun()
 
 
278
 
279
- with nav_col2:
 
280
  if st.session_state.current_page + pages_per_load < doc.page_count:
281
- if st.button("Next Pages"):
282
  st.session_state.current_page = min(
283
  doc.page_count - 1,
284
  st.session_state.current_page + pages_per_load
285
  )
286
  st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  else:
288
  st.info("Please upload a PDF file to begin translation")
289
 
 
6
  import pymupdf
7
  from deep_translator import (
8
  GoogleTranslator,
 
9
  )
10
+ from deep_translator.openai_compatible import OpenAICompatibleTranslator
11
+ import logging
12
 
13
  # Constants
14
  DEFAULT_PAGES_PER_LOAD = 2
 
17
 
18
  # Supported translators
19
  TRANSLATORS = {
20
+ 'OpenAI': OpenAICompatibleTranslator,
21
+ 'OpenAI Compatible': OpenAICompatibleTranslator,
22
  'google': GoogleTranslator,
 
23
  }
24
 
25
  # Color options
 
62
  cache_dir.mkdir(exist_ok=True)
63
  return cache_dir
64
 
65
+ def get_cache_key(doc_info: dict, page_num: int, translator_name: str, target_lang: str, text_content: str):
66
  """Generate cache key for a specific page translation"""
67
+ # 使用文档信息和页面内容的组合生成唯一标识
68
+ content_hash = hashlib.md5(text_content.encode('utf-8')).hexdigest()[:8]
69
+ doc_id = f"{doc_info.get('title', '')}_{doc_info.get('author', '')}_{doc_info.get('pagecount', '')}"
70
+ doc_hash = hashlib.md5(doc_id.encode('utf-8')).hexdigest()[:8]
71
+ return f"{doc_hash}_{content_hash}_page{page_num}_{translator_name}_{target_lang}.pdf"
72
 
73
  def get_cached_translation(cache_key: str) -> pymupdf.Document:
74
  """Get cached translation if exists"""
75
  cache_path = get_cache_dir() / cache_key
76
  if cache_path.exists():
77
+ try:
78
+ return pymupdf.open(str(cache_path))
79
+ except Exception as e:
80
+ logging.error(f"Error loading cache: {str(e)}")
81
+ return None
82
  return None
83
 
84
  def save_translation_cache(doc: pymupdf.Document, cache_key: str):
85
  """Save translation to cache"""
86
  cache_path = get_cache_dir() / cache_key
87
+ doc.save(str(cache_path)) # 确保提供文件路径字符串
88
 
89
  def translate_pdf_pages(doc, doc_bytes, start_page, num_pages, translator, text_color, translator_name, target_lang):
90
  """Translate specific pages of a PDF document with progress and caching"""
 
92
  rgb_color = COLOR_MAP.get(text_color.lower(), COLOR_MAP["darkred"])
93
 
94
  translated_pages = []
95
+ total_pages = min(start_page + num_pages, doc.page_count) - start_page
96
+ cache_hits = 0
97
 
98
  # Create a progress bar
99
  progress_bar = st.progress(0)
 
102
  for i, page_num in enumerate(range(start_page, min(start_page + num_pages, doc.page_count))):
103
  status_text.text(f"Translating page {page_num + 1}...")
104
 
105
+ # Extract text content for cache key
106
+ page = doc[page_num]
107
+ text_content = page.get_text("text")
108
+
109
+ # Check cache first using text content
110
+ cache_key = get_cache_key(
111
+ doc.metadata,
112
+ page_num,
113
+ translator_name,
114
+ target_lang,
115
+ text_content
116
+ )
117
+
118
  cached_doc = get_cached_translation(cache_key)
119
 
120
  if cached_doc is not None:
121
  translated_pages.append(cached_doc)
122
+ cache_hits += 1
123
+ logging.info(f"Cache hit: Using cached translation for page {page_num + 1}")
124
+ status_text.text(f"Using cached translation for page {page_num + 1}")
125
  else:
126
+ logging.info(f"Cache miss: Translating page {page_num + 1}")
127
+ status_text.text(f"Translating page {page_num + 1} (not in cache)")
128
+
129
  # Create a new PDF document for this page
130
  new_doc = pymupdf.open()
131
  new_doc.insert_pdf(doc, from_page=page_num, to_page=page_num)
 
137
  for block in blocks:
138
  bbox = block[:4]
139
  text = block[4]
 
 
140
  translated = translator.translate(text)
141
 
142
  # Cover original text with white and add translation in color
 
150
  # Save to cache
151
  save_translation_cache(new_doc, cache_key)
152
  translated_pages.append(new_doc)
153
+ logging.info(f"Cached new translation for page {page_num + 1}")
154
 
155
  # Update progress
156
+ progress = (i + 1) / total_pages
157
  progress_bar.progress(progress)
158
 
159
+ # Clear progress indicators and show summary
160
  progress_bar.empty()
161
+ if cache_hits > 0:
162
+ st.info(f"Used cache for {cache_hits} out of {total_pages} pages")
163
 
164
  return translated_pages
165
 
166
+ def get_page_image(page, scale=2):
167
  """Get high quality image from PDF page"""
168
  # 计算缩放后的尺寸
169
  zoom = scale
170
  mat = pymupdf.Matrix(zoom, zoom)
171
 
172
+ # 使用较低分辨率渲染页面,但保持清晰度
173
+ pix = page.get_pixmap(
174
+ matrix=mat,
175
+ alpha=False,
176
+ colorspace="rgb", # Use RGB instead of RGBA
177
+ )
178
 
179
  return pix
180
 
181
+ def translate_all_pages(
182
+ input_doc,
183
+ output_doc,
184
+ translator,
185
+ progress_bar,
186
+ batch_size=1,
187
+ **kwargs
188
+ ):
189
+ """Translate all pages of the PDF document"""
190
+ # Define colors
191
+ WHITE = pymupdf.pdfcolor["white"]
192
+ rgb_color = COLOR_MAP.get(kwargs.get('text_color', 'darkred').lower(), COLOR_MAP["darkred"])
193
+
194
+ total_pages = input_doc.page_count
195
+
196
+ # Create a progress bar for overall progress
197
+ status_text = st.empty()
198
+
199
+ # Translate all pages using translate_pdf_pages
200
+ translated_pages = translate_pdf_pages(
201
+ input_doc,
202
+ None, # doc_bytes not needed as we're using text content for cache
203
+ 0, # start from first page
204
+ total_pages, # translate all pages
205
+ translator,
206
+ kwargs.get('text_color', 'darkred'),
207
+ kwargs.get('translator_name', 'google'),
208
+ kwargs.get('target_lang', 'zh-CN')
209
+ )
210
+
211
+ # Combine all pages into one PDF with compression
212
+ output_path = kwargs.get('output_path', 'output.pdf')
213
+ for trans_doc in translated_pages:
214
+ output_doc.insert_pdf(trans_doc)
215
+
216
+ # Save with compression options
217
+ output_doc.save(
218
+ output_path,
219
+ garbage=4,
220
+ deflate=True,
221
+ clean=True,
222
+ linear=True
223
+ )
224
+
225
+ return output_doc
226
+
227
  def main():
228
  st.set_page_config(layout="wide", page_title="PDF Translator for Human: with Local-LLM/GPT")
229
  st.title("PDF Translator for Human: with Local-LLM/GPT")
 
261
  index=0
262
  )
263
 
264
+ # OpenAI specific settings
265
+ if translator_name in ['OpenAI', 'OpenAI Compatible']:
266
+ st.subheader(f"{translator_name} Settings")
267
  target_lang = st.selectbox(
268
  "Target Language",
269
  options=list(LANGUAGE_OPTIONS.keys()),
270
  index=0
271
  )
272
+
273
+ # Only show API key input if not set in environment
274
+ if not os.getenv("OPENAI_API_KEY"):
275
+ api_key = st.text_input(
276
+ "API Key",
277
+ value="",
278
+ type="password"
279
+ )
280
+ os.environ["OPENAI_API_KEY"] = api_key
281
+
282
+ # Different default API base for OpenAI and OpenAI Compatible
283
+ default_base = "https://api.openai.com/v1" if translator_name == 'OpenAI' else DEFAULT_API_BASE
284
  api_base = st.text_input(
285
  "API Base URL",
286
+ value=os.getenv("OPENAI_API_BASE", default_base)
287
  )
288
+
289
+ # Different default model for OpenAI and OpenAI Compatible
290
+ default_model = "gpt-4o-mini" if translator_name == 'OpenAI' else DEFAULT_MODEL
291
  model = st.text_input(
292
  "Model Name",
293
+ value=os.getenv("OPENAI_MODEL", default_model)
294
  )
295
 
296
  # Update environment variables
 
297
  os.environ["OPENAI_API_BASE"] = api_base
298
  os.environ["OPENAI_MODEL"] = model
299
  target_lang = LANGUAGE_OPTIONS[target_lang]
300
  else:
301
+ # For Google Translator, show target language selection
302
  target_lang_name = st.selectbox(
303
  "Target Language",
304
  options=list(SOURCE_LANGUAGE_OPTIONS.keys())[:-1], # Remove "Auto" option
 
319
  st.session_state.current_page = 0
320
  st.session_state.translation_started = True # 自动开始翻译
321
 
322
+ # Initialize translation status
323
+ if 'all_translated' not in st.session_state:
324
+ st.session_state.all_translated = False
325
+ if 'translated_doc' not in st.session_state:
326
+ st.session_state.translated_doc = None
327
+
328
  # Display original pages immediately
329
  with col1:
330
  st.header("Original")
 
360
  pix = get_page_image(page)
361
  st.image(pix.tobytes(), caption=f"Page {st.session_state.current_page + i + 1}", use_container_width=True)
362
 
363
+ # Navigation and action buttons in one row
364
+ st.markdown("---") # Add a separator
365
+ button_col1, button_col2, button_col3, button_col4 = st.columns(4)
366
+
367
+ # Previous Pages button
368
+ with button_col1:
369
  if st.session_state.current_page > 0:
370
+ if st.button("Previous Pages", use_container_width=True):
371
  st.session_state.current_page = max(0, st.session_state.current_page - pages_per_load)
372
  st.rerun()
373
+ else:
374
+ st.button("Previous Pages", disabled=True, use_container_width=True)
375
 
376
+ # Next Pages button
377
+ with button_col2:
378
  if st.session_state.current_page + pages_per_load < doc.page_count:
379
+ if st.button("Next Pages", use_container_width=True):
380
  st.session_state.current_page = min(
381
  doc.page_count - 1,
382
  st.session_state.current_page + pages_per_load
383
  )
384
  st.rerun()
385
+ else:
386
+ st.button("Next Pages", disabled=True, use_container_width=True)
387
+
388
+ # Translate All button
389
+ with button_col3:
390
+ if st.button("Translate All",
391
+ disabled=st.session_state.all_translated,
392
+ use_container_width=True):
393
+ # Configure translator
394
+ TranslatorClass = TRANSLATORS[translator_name]
395
+ translator = TranslatorClass(source=source_lang, target=target_lang)
396
+
397
+ # Translate all pages
398
+ output_doc = pymupdf.open()
399
+ output_path = f"translated_{uploaded_file.name}"
400
+ output_doc = translate_all_pages(
401
+ doc,
402
+ output_doc,
403
+ translator,
404
+ st.empty(),
405
+ pages_per_load,
406
+ text_color=text_color,
407
+ translator_name=translator_name,
408
+ target_lang=target_lang,
409
+ output_path=output_path # 提供输出路径
410
+ )
411
+
412
+ st.session_state.all_translated = True
413
+ st.session_state.translated_doc = output_path
414
+ st.rerun()
415
+
416
+ # Download button
417
+ with button_col4:
418
+ if not st.session_state.all_translated:
419
+ st.markdown(
420
+ """
421
+ <div title="You can download the translated file after all content has been translated">
422
+ <button style="width: 100%" disabled>Download</button>
423
+ </div>
424
+ """,
425
+ unsafe_allow_html=True
426
+ )
427
+ else:
428
+ with open(st.session_state.translated_doc, "rb") as file:
429
+ st.download_button(
430
+ "Download",
431
+ file,
432
+ file_name=f"translated_{uploaded_file.name}",
433
+ mime="application/pdf",
434
+ use_container_width=True
435
+ )
436
  else:
437
  st.info("Please upload a PDF file to begin translation")
438