From 0d87b10ee7371dda6ccf1f4d08ec6d890f8384f5 Mon Sep 17 00:00:00 2001 From: seidnerj Date: Fri, 29 Nov 2024 18:20:48 +0200 Subject: [PATCH 1/2] improved prompt for Anthropic's Claude, Updated prompt for ChatGTP & Gemini, Fixed minor typos + update tests --- engines/anthropic.py | 8 +-- engines/google.py | 18 ++++-- engines/openai.py | 15 ++++- tests/test_engine.py | 129 +++++++++++++++++++++++++------------------ 4 files changed, 104 insertions(+), 66 deletions(-) diff --git a/engines/anthropic.py b/engines/anthropic.py index 73828e4..2342411 100644 --- a/engines/anthropic.py +++ b/engines/anthropic.py @@ -35,10 +35,10 @@ class ClaudeTranslate(Base): 'Translate the given content from to only. Do not ' 'explain any term or answer any question-like content. Your answer ' 'should be solely the translation of the given content. In your answer ' - 'do not add any prefix or suffix to the translated content.') - - # https://docs.anthropic.com/en/docs/about-claude/models#model-names - models = [ + 'do not add any prefix or suffix to the translated content. Websites\' ' + 'URLs/addresses should be preserved as is in the translation\'s output. ') + + models = [ # https://docs.anthropic.com/en/docs/about-claude/models#model-names 'claude-3-5-sonnet-latest', 'claude-3-5-sonnet-20241022', 'claude-3-5-sonnet-20240620', diff --git a/engines/google.py b/engines/google.py index c105828..d7af7f4 100644 --- a/engines/google.py +++ b/engines/google.py @@ -246,14 +246,20 @@ class GeminiTranslate(Base): request_timeout = 30.0 prompt = ( - 'You are a meticulous translator who translates any given content ' - 'from to only. Do not provide any explanations and do ' - 'not answer any questions. Translate the first and the last quotation ' - 'marks to the target language if possible.') + 'You are a meticulous translator who translates any given content. ' + 'Translate the given content from to only. Do not ' + 'explain any term or answer any question-like content. Your answer ' + 'should be solely the translation of the given content. In your answer ' + 'do not add any prefix or suffix to the translated content. Websites\' ' + 'URLs/addresses should be preserved as is in the translation\'s output. ') + models = [ - 'gemini-1.5-flash', 'gemini-1.5-flash-8b', 'gemini-1.5-pro', + 'gemini-1.5-flash', + 'gemini-1.5-flash-8b', + 'gemini-1.5-pro', 'gemini-1.0-pro'] - model = 'gemini-1.5-flash' + + model = models[0] temperature = 0.9 top_p = 1.0 top_k = 1 diff --git a/engines/openai.py b/engines/openai.py index d6cd482..78cba45 100644 --- a/engines/openai.py +++ b/engines/openai.py @@ -39,10 +39,19 @@ class ChatgptTranslate(Base): prompt = ( 'You are a meticulous translator who translates any given content. ' 'Translate the given content from to only. Do not ' - 'explain any term or answer any question-like content.') + 'explain any term or answer any question-like content. Your answer ' + 'should be solely the translation of the given content. In your answer ' + 'do not add any prefix or suffix to the translated content. Websites\' ' + 'URLs/addresses should be preserved as is in the translation\'s output. ') + models = [ - 'gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-4', 'gpt-3.5-turbo'] - model = 'gpt-4o' + 'gpt-4o', + 'gpt-4o-mini', + 'gpt-4-turbo', + 'gpt-4', + 'gpt-3.5-turbo'] + + model = models[0] samplings = ['temperature', 'top_p'] sampling = 'temperature' temperature = 1.0 diff --git a/tests/test_engine.py b/tests/test_engine.py index 5931ceb..8c9b8b1 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -4,10 +4,10 @@ import unittest from pathlib import Path from types import GeneratorType -from unittest.mock import call, patch, Mock +from unittest.mock import patch, Mock from mechanize import HTTPError -from mechanize._response import closeable_response as Response +from mechanize._response import closeable_response as mechanize_response from ..lib.cache import Paragraph from ..lib.exception import UnexpectedResult, UnsupportedModel @@ -200,7 +200,7 @@ def test_translate(self, mock_request): @patch(module_name + '.base.request') def test_translate_with_stream(self, mock_request): self.translator.stream = True - mock_response = Mock(Response) + mock_response = Mock(mechanize_response) mock_request.return_value = mock_response self.assertIs(mock_response, self.translator.translate('Hello World')) @@ -226,7 +226,7 @@ def test_translate_with_http_error(self, mock_request): @patch(module_name + '.base.request') def test_translate_with_http_stream_parse_error(self, mock_request): self.translator.stream = True - mock_response = Mock(Response) + mock_response = Mock(mechanize_response) mock_request.return_value = mock_response with patch.object(self.translator, 'get_result') as mock_get_result: @@ -277,7 +277,7 @@ def test_translate_swap_api_keys_when_unavailable( def test_translate_swap_api_keys_with_http_error( self, mock_request, mock_need_swap_api_key, mock_swap_api_key): self.translator.stream = True - mock_response = Mock(Response) + mock_response = Mock(mechanize_response) mock_request.side_effect = [HTTPError, HTTPError, mock_response] mock_need_swap_api_key.return_value = True mock_swap_api_key.return_value = True @@ -290,7 +290,7 @@ def test_translate_swap_api_keys_with_http_error( @patch(module_name + '.base.request') def test_translate_swap_api_keys_with_http_error_without_result( self, mock_request, mock_need_swap_api_key, mock_swap_api_key): - mock_request.side_effect = HTTPError( + mock_request.side_effect = HTTPError( 'https://example.com/api', 409, 'Too many requests', {}, io.BytesIO(b'{"error": "any error"}')) mock_need_swap_api_key.side_effect = [True, True, False] @@ -389,15 +389,18 @@ def setUp(self): self.translator.set_target_lang('Chinese') def test_get_body(self): + model = 'gpt-4o' self.assertEqual(self.translator.get_body('test content'), json.dumps({ - 'model': 'gpt-4o', + 'model': model, 'messages': [ { 'role': 'system', - 'content': 'You are a meticulous translator who ' - 'translates any given content. Translate the given ' - 'content from English to Chinese only. Do not explain ' - 'any term or answer any question-like content.' + 'content': 'You are a meticulous translator who translates any given content. ' + 'Translate the given content from English to Chinese only. Do not ' + 'explain any term or answer any question-like content. Your answer ' + 'should be solely the translation of the given content. In your answer ' + 'do not add any prefix or suffix to the translated content. Websites\' ' + 'URLs/addresses should be preserved as is in the translation\'s output. ' }, { 'role': 'user', @@ -405,46 +408,52 @@ def test_get_body(self): } ], 'stream': True, - 'temperature': 1.0})) + 'temperature': 1.0 + })) def test_get_body_without_stream(self): + model = 'gpt-4o' self.translator.stream = False self.assertEqual( self.translator.get_body('test content'), json.dumps({ - 'model': 'gpt-4o', + 'model': model, 'messages': [ { 'role': 'system', - 'content': 'You are a meticulous translator who ' - 'translates any given content. Translate the given ' - 'content from English to Chinese only. Do not explain ' - 'any term or answer any question-like content.' + 'content': 'You are a meticulous translator who translates any given content. ' + 'Translate the given content from English to Chinese only. Do not ' + 'explain any term or answer any question-like content. Your answer ' + 'should be solely the translation of the given content. In your answer ' + 'do not add any prefix or suffix to the translated content. Websites\' ' + 'URLs/addresses should be preserved as is in the translation\'s output. ' }, { 'role': 'user', 'content': 'test content' } ], - 'temperature': 1.0, + 'temperature': 1.0 })) @patch(module_name + '.openai.EbookTranslator') @patch(module_name + '.base.request') def test_translate_stream(self, mock_request, mock_et): - url = 'https://api.openai.com/v1/chat/completions' + model = 'gpt-4o' prompt = ( 'You are a meticulous translator who translates any given ' 'content. Translate the given content from English to Chinese ' 'only. Do not explain any term or answer any question-like ' - 'content.') + 'content. Your answer should be solely the translation of the ' + 'given content. In your answer ' + 'do not add any prefix or suffix to the translated content. Websites\' ' + 'URLs/addresses should be preserved as is in the translation\'s output. ') data = json.dumps({ - 'model': 'gpt-4o', - 'messages': [ - {'role': 'system', 'content': prompt}, - {'role': 'user', 'content': 'Hello World!'}], + 'model': model, + 'messages': [{'role': 'system', 'content': prompt}, {'role': 'user', 'content': 'Hello World!'}], 'stream': True, - 'temperature': 1.0}) + 'temperature': 1.0, + }) mock_et.__version__ = '1.0.0' headers = { 'Content-Type': 'application/json', @@ -456,6 +465,7 @@ def test_translate_stream(self, mock_request, mock_et): template % i.encode() for i in '你好世界!'] \ + ['data: [DONE]'.encode()] mock_request.return_value = mock_response + url = 'https://api.openai.com/v1/chat/completions' result = self.translator.translate('Hello World!') mock_request.assert_called_with( @@ -500,7 +510,7 @@ def test_created_translator(self): 'https://api.openai.com/v1/batches') @patch(module_name + '.openai.request') - def test_supportd_models(self, mock_request): + def test_supported_models(self, mock_request): mock_request.return_value = """ { "object": "list", @@ -532,8 +542,9 @@ def test_supportd_models(self, mock_request): ['model-id-0', 'model-id-1', 'model-id-2']) @patch(module_name + '.openai.ChatgptBatchTranslate.supported_models') - def test_upload_with_unsupported_model(self, mock_suppored_models): - mock_suppored_models.return_value = ['gpt-4o'] + def test_upload_with_unsupported_model(self, mock_supported_models): + model = 'gpt-4o' + mock_supported_models.return_value = [model] self.mock_translator.model = 'fake-model' self.mock_translator.stream = True with self.assertRaises(UnsupportedModel) as cm: @@ -545,7 +556,7 @@ def test_upload_with_unsupported_model(self, mock_suppored_models): @patch.object(ChatgptBatchTranslate, 'boundary', new='xxxxxxxxxx') @patch(module_name + '.openai.ChatgptBatchTranslate.supported_models') @patch(module_name + '.openai.request') - def test_upload(self, mock_request, mock_suppored_models): + def test_upload(self, mock_request, mock_supported_models): mock_request.return_value = """ { "id": "test-file-id", @@ -556,7 +567,8 @@ def test_upload(self, mock_request, mock_suppored_models): "purpose": "fine-tune" } """ - mock_suppored_models.return_value = ['gpt-4o'] + model = 'gpt-4o' + mock_supported_models.return_value = [model] mock_paragraph_1 = Mock(Paragraph) mock_paragraph_1.md5 = 'abc' @@ -564,16 +576,17 @@ def test_upload(self, mock_request, mock_suppored_models): mock_paragraph_2 = Mock(Paragraph) mock_paragraph_2.md5 = 'def' mock_paragraph_2.original = 'test content 2' - self.mock_translator.model = 'gpt-4o' + self.mock_translator.model = model self.mock_translator.api_key = 'abc' def mock_get_body(text): return json.dumps({ - 'model': 'gpt-4o', + 'model': model, 'messages': [ {'role': 'system', 'content': 'some prompt...'}, {'role': 'user', 'content': text}], - 'temperature': 1.0}) + 'temperature': 1.0 + }) self.mock_translator.get_body.side_effect = mock_get_body file_id = self.batch_translator.upload( @@ -590,13 +603,13 @@ def mock_get_body(text): 'Content-Type: application/json\r\n' '\r\n{"custom_id": "abc", "method": "POST", ' '"url": "/v1/chat/completions", ' - '"body": {"model": "gpt-4o", ' + '"body": {"model": "' + model + '", ' '"messages": [{"role": "system", ' '"content": "some prompt..."}, {"role": "user", ' '"content": "test content 1"}], "temperature": 1.0}}\n' '{"custom_id": "def", "method": "POST", ' '"url": "/v1/chat/completions", ' - '"body": {"model": "gpt-4o", ' + '"body": {"model": "' + model + '", ' '"messages": [{"role": "system", ' '"content": "some prompt..."}, {"role": "user", ' '"content": "test content 2"}], "temperature": 1.0}}\r\n' @@ -781,18 +794,20 @@ def setUp(self): @patch(module_name + '.base.request') def test_translate_stream(self, mock_request): + model = 'gpt-35-turbo' prompt = ( 'You are a meticulous translator who translates any given ' 'content. Translate the given content from English to Chinese ' 'only. Do not explain any term or answer any question-like ' - 'content.') + 'content. Your answer should be solely the translation of the ' + 'given content. In your answer ' + 'do not add any prefix or suffix to the translated content. Websites\' ' + 'URLs/addresses should be preserved as is in the translation\'s output. ') data = json.dumps({ 'stream': True, - 'messages': [ - {'role': 'system', 'content': prompt}, - {'role': 'user', 'content': 'Hello World!'} - ], - 'temperature': 1.0}) + 'messages': [{'role': 'system', 'content': prompt}, {'role': 'user', 'content': 'Hello World!'}], + 'temperature': 1.0 + }) headers = { 'Content-Type': 'application/json', 'api-key': 'a'} @@ -804,9 +819,10 @@ def test_translate_stream(self, mock_request): + ['data: [DONE]'.encode()] mock_request.return_value = mock_response url = ('https://docs-test-001.openai.azure.com/openai/deployments/' - 'gpt-35-turbo/chat/completions?api-version=2023-05-15') + f'{model}/chat/completions?api-version=2023-05-15') self.translator.endpoint = url result = self.translator.translate('Hello World!') + mock_request.assert_called_with( url=url, data=data, headers=headers, method='POST', timeout=30.0, proxy_uri=None, raw_object=True) @@ -828,21 +844,24 @@ def setUp(self): @patch(module_name + '.anthropic.EbookTranslator') @patch(module_name + '.base.request') def test_translate(self, mock_request, mock_et): + model = 'claude-3-5-sonnet-20241022' prompt = ( 'You are a meticulous translator who translates any given ' 'content. Translate the given content from English to Chinese ' 'only. Do not explain any term or answer any question-like ' 'content. Your answer should be solely the translation of the ' - 'given content. In your answer do not add any prefix or suffix to ' - 'the translated content.') + 'given content. In your answer ' + 'do not add any prefix or suffix to the translated content. Websites\' ' + 'URLs/addresses should be preserved as is in the translation\'s output. ') data = json.dumps({ 'stream': False, 'max_tokens': 4096, - 'model': 'claude-2.1', + 'model': model, 'top_k': 1, 'system': prompt, 'messages': [{'role': 'user', 'content': 'Hello World!'}], - 'temperature': 1.0}) + 'temperature': 1.0 + }) mock_et.__version__ = '1.0.0' headers = { 'Content-Type': 'application/json', @@ -859,7 +878,7 @@ def test_translate(self, mock_request, mock_et): } ], "id": "msg_013Zva2CMHLNnXjNJJKqJ2EF", - "model": "claude-3-opus-20240229", + "model": "{""" + model + """}", "role": "assistant", "stop_reason": "end_turn", "stop_sequence": null, @@ -874,7 +893,7 @@ def test_translate(self, mock_request, mock_et): url = 'https://api.anthropic.com/v1/messages' self.translator.endpoint = url self.translator.stream = False - self.translator.model = 'claude-2.1' + self.translator.model = model result = self.translator.translate('Hello World!') mock_request.assert_called_with( @@ -885,21 +904,24 @@ def test_translate(self, mock_request, mock_et): @patch(module_name + '.anthropic.EbookTranslator') @patch(module_name + '.base.request') def test_translate_stream(self, mock_request, mock_et): + model = 'claude-3-5-sonnet-20241022' prompt = ( 'You are a meticulous translator who translates any given ' 'content. Translate the given content from English to Chinese ' 'only. Do not explain any term or answer any question-like ' 'content. Your answer should be solely the translation of the ' - 'given content. In your answer do not add any prefix or suffix to ' - 'the translated content.') + 'given content. In your answer ' + 'do not add any prefix or suffix to the translated content. Websites\' ' + 'URLs/addresses should be preserved as is in the translation\'s output. ') data = json.dumps({ 'stream': True, 'max_tokens': 4096, - 'model': 'claude-2.1', + 'model': model, 'top_k': 1, 'system': prompt, 'messages': [{'role': 'user', 'content': 'Hello World!'}], - 'temperature': 1.0}) + 'temperature': 1.0 + }) mock_et.__version__ = '1.0.0' headers = { 'Content-Type': 'application/json', @@ -946,8 +968,9 @@ def test_translate_stream(self, mock_request, mock_et): mock_request.return_value = mock_response url = 'https://api.anthropic.com/v1/messages' self.translator.endpoint = url - self.translator.model = 'claude-2.1' + self.translator.model = model result = self.translator.translate('Hello World!') + mock_request.assert_called_with( url=url, data=data, headers=headers, method='POST', timeout=30.0, proxy_uri=None, raw_object=True) From 8a12aae6ef8b085ba015e23a79651b8c2a1ac1e7 Mon Sep 17 00:00:00 2001 From: seidnerj Date: Sat, 7 Dec 2024 18:50:15 +0200 Subject: [PATCH 2/2] automatically set directionality if pre-defined in languages.py show output format selection box in advanced mode window some minor bug fixes some improvements to language used in titles etc. fixed minor typos extracted Ebook class from within Ebooks class Added typing hints on Ebooks class --- advanced.py | 113 ++++++++++++++++++++++++--------------- batch.py | 74 ++++++++++++++----------- engines/anthropic.py | 6 ++- engines/baidu.py | 2 + engines/base.py | 4 ++ engines/deepl.py | 3 ++ engines/google.py | 5 ++ engines/languages.py | 11 +++- engines/microsoft.py | 4 +- engines/openai.py | 2 + engines/youdao.py | 2 + lib/ebook.py | 87 ++++++++++++++++-------------- tests/test_conversion.py | 4 +- ui.py | 4 +- 14 files changed, 197 insertions(+), 124 deletions(-) diff --git a/advanced.py b/advanced.py index 0e05a8f..fa36224 100644 --- a/advanced.py +++ b/advanced.py @@ -247,6 +247,7 @@ def __init__(self, parent, ebook): layout.addWidget(self.start_button) def layout_format(self): + engine_class = get_engine_class() widget = QWidget() layout = QHBoxLayout(widget) layout.setContentsMargins(0, 0, 0, 0) @@ -258,6 +259,13 @@ def layout_format(self): input_layout.addWidget(input_format) layout.addWidget(input_group) + output_group = QGroupBox(_('Output Format')) + output_layout = QGridLayout(output_group) + output_format = OutputFormat() + output_format.setFixedWidth(150) + output_layout.addWidget(output_format) + layout.addWidget(output_group) + source_group = QGroupBox(_('Source Language')) source_layout = QVBoxLayout(source_group) source_lang = SourceLang() @@ -272,6 +280,35 @@ def layout_format(self): target_layout.addWidget(target_lang) layout.addWidget(target_group) + source_lang.refresh.emit( + engine_class.lang_codes.get('source'), + engine_class.config.get('source_lang'), + not issubclass(engine_class, CustomTranslate)) + + target_lang.refresh.emit( + engine_class.lang_codes.get('target'), + engine_class.config.get('target_lang')) + + def change_input_format(_format): + self.ebook.set_input_format(_format) + change_input_format(input_format.currentText()) + input_format.currentTextChanged.connect(change_input_format) + + def change_output_format(_format): + self.ebook.set_output_format(_format) + change_output_format(output_format.currentText()) + output_format.currentTextChanged.connect(change_output_format) + + def change_source_lang(lang): + self.ebook.set_source_lang(lang) + change_source_lang(source_lang.currentText()) + source_lang.currentTextChanged.connect(change_source_lang) + + def change_target_lang(lang): + self.ebook.set_target_lang(lang) + change_target_lang(target_lang.currentText()) + target_lang.currentTextChanged.connect(change_target_lang) + if self.ebook.input_format in extra_formats.keys(): encoding_group = QGroupBox(_('Encoding')) encoding_layout = QVBoxLayout(encoding_group) @@ -295,34 +332,17 @@ def change_encoding(encoding): direction_layout.addWidget(direction_list) layout.addWidget(direction_group) - def change_direction(index): - direction = direction_list.itemData(index) - self.ebook.set_target_direction(direction) + def change_direction(_index): + _direction = direction_list.itemData(_index) + self.ebook.set_target_direction(_direction) direction_list.currentIndexChanged.connect(change_direction) - def change_input_format(format): - self.ebook.set_input_format(format) - change_input_format(input_format.currentText()) - input_format.currentTextChanged.connect(change_input_format) - - engine_class = get_engine_class() - source_lang.refresh.emit( - engine_class.lang_codes.get('source'), - engine_class.config.get('source_lang'), - not issubclass(engine_class, CustomTranslate)) - target_lang.refresh.emit( - engine_class.lang_codes.get('target'), - engine_class.config.get('target_lang')) - - def change_source_lang(lang): - self.ebook.set_source_lang(source_lang.currentText()) - change_source_lang(source_lang.currentText()) - source_lang.currentTextChanged.connect(change_source_lang) - - def change_target_lang(lang): - self.ebook.set_target_lang(lang) - change_target_lang(target_lang.currentText()) - target_lang.currentTextChanged.connect(change_target_lang) + engine_target_lange_codes = engine_class.lang_codes.get('target') + if engine_target_lange_codes is not None and self.ebook.target_lang in engine_target_lange_codes: + target_lang_code = engine_target_lange_codes[self.ebook.target_lang] + direction = engine_class.lang_codes_directionality.get(target_lang_code, 'auto') + index = direction_list.findData(direction) + direction_list.setCurrentIndex(index) return widget @@ -356,7 +376,7 @@ def __init__(self, parent, icon, worker, ebook): self.cache = None self.merge_enabled = False - self.prgress_step = 0 + self.progress_step = 0 self.translate_all = False self.editor_worker = EditorWorker() @@ -612,7 +632,7 @@ def layout_table(self): progress_bar.setVisible(False) def write_progress(): - value = progress_bar.value() + self.prgress_step + value = progress_bar.value() + self.progress_step if value > progress_bar.maximum(): value = progress_bar.maximum() progress_bar.setValue(value) @@ -941,9 +961,12 @@ def layout_review(self): translation_text.ensureCursorVisible) def refresh_translation(paragraph): - raw_text.setPlainText(paragraph.raw.strip()) - original_text.setPlainText(paragraph.original.strip()) - translation_text.setPlainText(paragraph.translation) + # TODO: how can this happen and what should we do in case it does? + if paragraph is not None: + raw_text.setPlainText(paragraph.raw.strip()) + original_text.setPlainText(paragraph.original.strip()) + translation_text.setPlainText(paragraph.translation) + self.paragraph_sig.connect(refresh_translation) self.trans_worker.start.connect( @@ -1060,17 +1083,19 @@ def modify_translation(): self.editor_worker.show.connect(save_status.setText) def save_translation(): - save_button.setDisabled(True) - paragraph = self.table.current_paragraph() - translation = translation_text.toPlainText() - paragraph.translation = translation - paragraph.engine_name = self.current_engine.name - paragraph.target_lang = self.ebook.target_lang - self.table.row.emit(paragraph.row) - self.cache.update_paragraph(paragraph) - translation_text.setFocus(Qt.OtherFocusReason) - self.editor_worker.start[str].emit( - _('Your changes have been saved.')) + # TODO: how can this happen and what should we do in case it does? + if paragraph is not None: + save_button.setDisabled(True) + paragraph = self.table.current_paragraph() + translation = translation_text.toPlainText() + paragraph.translation = translation + paragraph.engine_name = self.current_engine.name + paragraph.target_lang = self.ebook.target_lang + self.table.row.emit(paragraph.row) + self.cache.update_paragraph(paragraph) + translation_text.setFocus(Qt.OtherFocusReason) + self.editor_worker.start[str].emit(_('Your changes have been saved.')) + save_button.clicked.connect(save_translation) set_shortcut(save_button, 'save', save_translation, save_button.text()) @@ -1109,7 +1134,7 @@ def translate_all_paragraphs(self): is_fresh = len(paragraphs) < 1 if is_fresh: paragraphs = self.table.get_selected_paragraphs(False, True) - self.prgress_step = self.get_progress_step(len(paragraphs)) + self.progress_step = self.get_progress_step(len(paragraphs)) if not self.translate_all: message = _( 'Are you sure you want to translate all {:n} paragraphs?') @@ -1124,7 +1149,7 @@ def translate_selected_paragraph(self): if len(paragraphs) == self.table.rowCount(): self.translate_all_paragraphs() else: - self.prgress_step = self.get_progress_step(len(paragraphs)) + self.progress_step = self.get_progress_step(len(paragraphs)) self.trans_worker.translate.emit(paragraphs, True) def install_widget_event( diff --git a/batch.py b/batch.py index 4faa957..dfe1d19 100644 --- a/batch.py +++ b/batch.py @@ -5,6 +5,7 @@ from .lib.translation import get_engine_class from .lib.conversion import extra_formats from .lib.encodings import encoding_list +from .lib.ebook import Ebooks, Ebook from .engines.custom import CustomTranslate from .components import ( Footer, AlertMessage, SourceLang, TargetLang, InputFormat, @@ -24,7 +25,7 @@ class BatchTranslation(QDialog): - def __init__(self, parent, worker, ebooks): + def __init__(self, parent, worker, ebooks: Ebooks): QDialog.__init__(self, parent) self.gui = parent @@ -82,40 +83,10 @@ def layout_translate(self): translation_engine = get_engine_class() for row, ebook in enumerate(self.ebooks): + ebook: Ebook ebook_title = QTableWidgetItem(ebook.title) table.setItem(row, 0, ebook_title) - if ebook.input_format in extra_formats.keys(): - input_encoding = QComboBox() - input_encoding.wheelEvent = lambda event: None - table.setCellWidget(row, 1, self._cell_widget(input_encoding)) - input_encoding.addItems(encoding_list) - input_encoding.currentTextChanged.connect( - lambda encoding, row=row: self.ebooks[row] - .set_encoding(encoding)) - input_encoding.currentTextChanged.connect( - lambda encoding: input_encoding.setToolTip(encoding)) - # Target directionality - target_direction = QTableWidgetItem(_('Default')) - target_direction.setTextAlignment(Qt.AlignCenter) - table.setItem(row, 6, target_direction) - else: - input_encoding = QTableWidgetItem(_('Default')) - input_encoding.setTextAlignment(Qt.AlignCenter) - table.setItem(row, 1, input_encoding) - # Target directionality - direction_list = QComboBox() - direction_list.wheelEvent = lambda event: None - direction_list.addItem(_('Auto'), 'auto') - direction_list.addItem(_('Left to Right'), 'ltr') - direction_list.addItem(_('Right to Left'), 'rtl') - direction_list.currentIndexChanged.connect( - lambda index, row=row: self.ebooks[row] - .set_target_direction(direction_list.itemData(index))) - direction_list.currentTextChanged.connect( - lambda direction: direction_list.setToolTip(direction)) - table.setCellWidget(row, 6, self._cell_widget(direction_list)) - input_fmt = InputFormat(ebook.files.keys()) table.setCellWidget(row, 2, self._cell_widget(input_fmt)) @@ -175,6 +146,45 @@ def change_target_lang(lang, row=row): translation_engine.lang_codes.get('target'), translation_engine.config.get('target_lang')) + if ebook.input_format in extra_formats.keys(): + input_encoding = QComboBox() + input_encoding.wheelEvent = lambda event: None + table.setCellWidget(row, 1, self._cell_widget(input_encoding)) + input_encoding.addItems(encoding_list) + input_encoding.currentTextChanged.connect( + lambda encoding, row=row: self.ebooks[row] + .set_encoding(encoding)) + input_encoding.currentTextChanged.connect( + lambda encoding: input_encoding.setToolTip(encoding)) + # Target directionality + target_direction = QTableWidgetItem(_('Default')) + target_direction.setTextAlignment(Qt.AlignCenter) + table.setItem(row, 6, target_direction) + else: + input_encoding = QTableWidgetItem(_('Default')) + input_encoding.setTextAlignment(Qt.AlignCenter) + table.setItem(row, 1, input_encoding) + # Target directionality + direction_list = QComboBox() + direction_list.wheelEvent = lambda event: None + direction_list.addItem(_('Auto'), 'auto') + direction_list.addItem(_('Left to Right'), 'ltr') + direction_list.addItem(_('Right to Left'), 'rtl') + direction_list.currentIndexChanged.connect( + lambda index, row=row: self.ebooks[row] + .set_target_direction(direction_list.itemData(index))) + direction_list.currentTextChanged.connect( + lambda direction: direction_list.setToolTip(direction)) + + engine_target_lange_codes = translation_engine.lang_codes.get('target') + if engine_target_lange_codes is not None and ebook.target_lang in engine_target_lange_codes: + target_lang_code = engine_target_lange_codes[ebook.target_lang] + direction = translation_engine.lang_codes_directionality.get(target_lang_code, 'auto') + index = direction_list.findData(direction) + direction_list.setCurrentIndex(index) + + table.setCellWidget(row, 6, self._cell_widget(direction_list)) + table.resizeRowsToContents() table.resizeColumnsToContents() diff --git a/engines/anthropic.py b/engines/anthropic.py index 2342411..c5c3138 100644 --- a/engines/anthropic.py +++ b/engines/anthropic.py @@ -5,7 +5,8 @@ from mechanize._response import response_seek_wrapper as Response from .base import Base -from .languages import anthropic as anthropic_languages +from .languages import lang_directionality +from .languages import anthropic from .prompt_extensions import anthropic as anthropic_prompt_extension try: @@ -19,7 +20,8 @@ class ClaudeTranslate(Base): name = 'Claude' alias = 'Claude (Anthropic)' - lang_codes = Base.load_lang_codes(anthropic_languages) + lang_codes = Base.load_lang_codes(anthropic) + lang_codes_directionality = Base.load_lang_codes_directionality(lang_directionality) endpoint = 'https://api.anthropic.com/v1/messages' api_version = '2023-06-01' api_key_hint = 'sk-ant-xxxx' diff --git a/engines/baidu.py b/engines/baidu.py index d42b7c8..114f8af 100644 --- a/engines/baidu.py +++ b/engines/baidu.py @@ -7,6 +7,7 @@ from ..lib.exception import BadApiKeyFormat from .base import Base +from .languages import lang_directionality from .languages import baidu @@ -17,6 +18,7 @@ class BaiduTranslate(Base): name = 'Baidu' alias = _z('Baidu') lang_codes = Base.load_lang_codes(baidu) + lang_codes_directionality = Base.load_lang_codes_directionality(lang_directionality) endpoint = 'https://fanyi-api.baidu.com/api/trans/vip/translate' api_key_hint = 'appid|appkey' api_key_pattern = r'^[^\s:\|]+?[:\|][^\s:\|]+$' diff --git a/engines/base.py b/engines/base.py index 54bbb95..fc29ef1 100644 --- a/engines/base.py +++ b/engines/base.py @@ -69,6 +69,10 @@ def load_lang_codes(cls, codes): codes = {'source': codes, 'target': codes} return codes + @classmethod + def load_lang_codes_directionality(cls, codes): + return codes + @classmethod def get_source_code(cls, lang): source_codes = cls.lang_codes.get('source') diff --git a/engines/deepl.py b/engines/deepl.py index e9f639a..1f5ab87 100644 --- a/engines/deepl.py +++ b/engines/deepl.py @@ -5,6 +5,7 @@ from ..lib.utils import request from .base import Base +from .languages import lang_directionality from .languages import deepl @@ -15,6 +16,7 @@ class DeeplTranslate(Base): name = 'DeepL' alias = 'DeepL' lang_codes = Base.load_lang_codes(deepl) + lang_codes_directionality = Base.load_lang_codes_directionality(lang_directionality) endpoint = 'https://api-free.deepl.com/v2/translate' usage_endpoint = 'https://api-free.deepl.com/v2/usage' # api_key_hint = 'xxx-xxx-xxx:fx' @@ -64,6 +66,7 @@ class DeeplFreeTranslate(Base): alias = 'DeepL (Free)' free = True lang_codes = Base.load_lang_codes(deepl) + lang_codes_directionality = Base.load_lang_codes_directionality(lang_directionality) endpoint = 'https://www2.deepl.com/jsonrpc?client=chrome-extension,1.5.1' need_api_key = False placeholder = DeeplTranslate.placeholder diff --git a/engines/google.py b/engines/google.py index d7af7f4..fe3f612 100644 --- a/engines/google.py +++ b/engines/google.py @@ -9,6 +9,7 @@ from ..lib.utils import traceback_error from .base import Base +from .languages import lang_directionality from .languages import google, gemini @@ -25,6 +26,7 @@ class GoogleFreeTranslate(Base): alias = 'Google (Free)' free = True lang_codes = Base.load_lang_codes(google) + lang_codes_directionality = Base.load_lang_codes_directionality(lang_directionality) endpoint = 'https://translate.googleapis.com/translate_a/single' need_api_key = False @@ -154,6 +156,7 @@ class GoogleBasicTranslateADC(GoogleTranslateMixin, Base): name = 'Google(Basic)ADC' alias = 'Google (Basic) ADC' lang_codes = Base.load_lang_codes(google) + lang_codes_directionality = Base.load_lang_codes_directionality(lang_directionality) endpoint = 'https://translation.googleapis.com/language/translate/v2' api_key_hint = 'API key' need_api_key = False @@ -203,6 +206,7 @@ class GoogleAdvancedTranslate(GoogleTranslateMixin, Base): name = 'Google(Advanced)' alias = 'Google (Advanced) ADC' lang_codes = Base.load_lang_codes(google) + lang_codes_directionality = Base.load_lang_codes_directionality(lang_directionality) endpoint = 'https://translation.googleapis.com/v3/projects/{}' api_key_hint = 'PROJECT_ID' need_api_key = False @@ -237,6 +241,7 @@ class GeminiTranslate(Base): name = 'Gemini' alias = 'Gemini' lang_codes = Base.load_lang_codes(gemini) + lang_codes_directionality = Base.load_lang_codes_directionality(lang_directionality) endpoint = 'https://generativelanguage.googleapis.com/v1/' \ 'models/{}:{}?key={}' need_api_key = True diff --git a/engines/languages.py b/engines/languages.py index 4d12ac9..c9e844b 100644 --- a/engines/languages.py +++ b/engines/languages.py @@ -327,7 +327,7 @@ 'Hausa': 'ha', 'Hawaiian': 'haw', 'Hebrew': 'iw', - 'Hebrew (with Niqqud)': 'iwn', + 'Hebrew (with Niqqud)': 'iw', 'Hiligaynon': 'hil', 'Hindi': 'hi', 'Hmong': 'hmn', @@ -1038,3 +1038,12 @@ '祖鲁语': 'zul', '爪哇语': 'jav' } + +lang_directionality = { + 'iw': 'rtl', + 'ar': 'rtl', + 'en': 'ltr', + 'es': 'ltr', + 'it': 'ltr', + 'de': 'ltr' +} \ No newline at end of file diff --git a/engines/microsoft.py b/engines/microsoft.py index 562c790..425365b 100755 --- a/engines/microsoft.py +++ b/engines/microsoft.py @@ -4,8 +4,9 @@ from ..lib.utils import request -from .languages import microsoft from .base import Base +from .languages import lang_directionality +from .languages import microsoft from .openai import ChatgptTranslate @@ -22,6 +23,7 @@ class MicrosoftEdgeTranslate(Base): alias = 'Microsoft Edge (Free)' free = True lang_codes = Base.load_lang_codes(microsoft) + lang_codes_directionality = Base.load_lang_codes_directionality(lang_directionality) endpoint = 'https://api-edge.cognitive.microsofttranslator.com/translate' need_api_key = False access_info = None diff --git a/engines/openai.py b/engines/openai.py index 78cba45..5b49c53 100644 --- a/engines/openai.py +++ b/engines/openai.py @@ -10,6 +10,7 @@ from ..lib.exception import UnsupportedModel from .base import Base +from .languages import lang_directionality from .languages import google @@ -27,6 +28,7 @@ class ChatgptTranslate(Base): name = 'ChatGPT' alias = 'ChatGPT (OpenAI)' lang_codes = Base.load_lang_codes(google) + lang_codes_directionality = Base.load_lang_codes_directionality(lang_directionality) endpoint = 'https://api.openai.com/v1/chat/completions' # api_key_hint = 'sk-xxx...xxx' # https://help.openai.com/en/collections/3808446-api-error-codes-explained diff --git a/engines/youdao.py b/engines/youdao.py index cade7f9..47ae651 100644 --- a/engines/youdao.py +++ b/engines/youdao.py @@ -8,6 +8,7 @@ from ..lib.exception import BadApiKeyFormat from .base import Base +from .languages import lang_directionality from .languages import youdao @@ -18,6 +19,7 @@ class YoudaoTranslate(Base): name = 'Youdao' alias = _z('Youdao') lang_codes = Base.load_lang_codes(youdao) + lang_codes_directionality = Base.load_lang_codes_directionality(lang_directionality) endpoint = 'https://openapi.youdao.com/api' api_key_hint = 'appid|appsecret' api_key_pattern = r'^[^\s:\|]+?[:\|][^\s:\|]+$' diff --git a/lib/ebook.py b/lib/ebook.py index 1b863bf..8953314 100644 --- a/lib/ebook.py +++ b/lib/ebook.py @@ -1,59 +1,64 @@ -class Ebooks: - class Ebook: - def __init__(self, id, title, files, input_format, source_lang, - extra_formats=[]): - self.id = id - self.files = files - self.input_format = input_format - self.source_lang = source_lang - self.extra_formats = extra_formats +from typing import Iterator, Iterable - self.output_format = None - self.target_lang = None - self.lang_code = None - self.title = title - self.custom_title = None - self.encoding = 'utf-8' - self.target_direction = 'auto' +class Ebook: + def __init__(self, id, title, files, input_format, source_lang, + extra_formats=[]): + self.id = id + self.files = files + self.input_format = input_format + self.source_lang = source_lang + self.extra_formats = extra_formats - def set_input_format(self, format): - self.input_format = format + self.output_format = None + self.target_lang = None + self.lang_code = None - def set_output_format(self, format): - self.output_format = format + self.title = title + self.custom_title = None + self.encoding = 'utf-8' + self.target_direction = 'auto' - def set_source_lang(self, lang): - self.source_lang = lang + def set_input_format(self, format): + self.input_format = format - def set_target_lang(self, lang): - self.target_lang = lang + def set_output_format(self, format): + self.output_format = format - def set_lang_code(self, code): - self.lang_code = code + def set_source_lang(self, lang): + self.source_lang = lang - def set_custom_title(self, title): - self.custom_title = title + def set_target_lang(self, lang): + self.target_lang = lang - def set_encoding(self, encoding): - self.encoding = encoding + def set_lang_code(self, code): + self.lang_code = code - def set_target_direction(self, direction): - self.target_direction = direction + def set_custom_title(self, title): + self.custom_title = title - def get_input_path(self): - return self.files.get(self.input_format) + def set_encoding(self, encoding): + self.encoding = encoding - def is_extra_format(self): - return self.input_format in self.extra_formats + def set_target_direction(self, direction): + self.target_direction = direction + + def get_input_path(self): + return self.files.get(self.input_format) + + def is_extra_format(self): + return self.input_format in self.extra_formats + + +class Ebooks(Iterable): def __init__(self): - self.ebooks = [] + self.ebooks: list[Ebook] = [] def add(self, *args): - self.ebooks.append(self.Ebook(*args)) + self.ebooks.append(Ebook(*args)) - def first(self): + def first(self) -> Ebook: return self.ebooks.pop(0) def clear(self): @@ -62,9 +67,9 @@ def clear(self): def __len__(self): return len(self.ebooks) - def __iter__(self): + def __iter__(self) -> Iterator[Ebook]: for ebook in self.ebooks: yield ebook - def __getitem__(self, index): + def __getitem__(self, index) -> Ebook: return self.ebooks[index] diff --git a/tests/test_conversion.py b/tests/test_conversion.py index bd5b2c2..dee727e 100644 --- a/tests/test_conversion.py +++ b/tests/test_conversion.py @@ -4,7 +4,7 @@ from unittest.mock import call, patch, Mock from ..lib.conversion import ConversionWorker -from ..lib.ebook import Ebooks +from ..lib.ebook import Ebook module_name = 'calibre_plugins.ebook_translator.lib.conversion' @@ -18,7 +18,7 @@ def setUp(self): self.worker.db = Mock() self.worker.api = Mock() - self.ebook = Mock(Ebooks.Ebook) + self.ebook = Mock(Ebook) self.job = Mock() self.worker.working_jobs = { self.job: (self.ebook, str(Path('/path/to/test.epub')))} diff --git a/ui.py b/ui.py index 32f8e87..35411ba 100644 --- a/ui.py +++ b/ui.py @@ -86,10 +86,12 @@ def show_advanced_translation(self): ebooks = self.get_selected_ebooks() if len(ebooks) < 1: return self.alert.pop( - _('Please choose one single book.'), 'warning') + _('Please choose a book.'), 'warning') window = CreateTranslationProject(self.gui, ebooks.first()) window.start_translation.connect(self.advanced_translation_window) window.setModal(True) + window.setWindowTitle( + '%s - %s' % (_('Advanced Mode'), self.title)) window.setWindowTitle(self.title) window.show()