Skip to content

Commit

Permalink
Option to include original text in translated subtitles (#59)
Browse files Browse the repository at this point in the history
* include_original option

* Ensure false bools options are cleared on update

* Don't save project options as global options

* Tooltips for settings dialog
  • Loading branch information
machinewrapped authored Jul 22, 2023
1 parent 2c675f7 commit 32232a6
Show file tree
Hide file tree
Showing 8 changed files with 81 additions and 46 deletions.
4 changes: 2 additions & 2 deletions GUI/FileCommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def execute(self):

if self.project.subtitles.translated:
outputpath = GetOutputPath(self.project.projectfile)
self.project.subtitles.SaveTranslation(outputpath)
self.project.SaveSubtitles(outputpath)
return True

class SaveSubtitleFile(Command):
Expand All @@ -79,5 +79,5 @@ def __init__(self, filepath, project : SubtitleProject):
self.project = project

def execute(self):
self.project.subtitles.SaveTranslation(self.filepath)
self.project.SaveSubtitles(self.filepath)
return True
32 changes: 17 additions & 15 deletions GUI/SettingsDialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,33 @@ class SettingsDialog(QDialog):
'autosave': bool,
'write_backup': bool,
'stop_on_error': bool,
'max_threads': int
'max_threads': (int, "Maximum number of simultaneous translation threads for fast translation")
},
'GPT': {
'api_key': str,
'api_base': str,
'api_key': (str, "An OpenAI API key is required to use this program (https://platform.openai.com/account/api-keys)"),
'api_base': (str, "The base URI to use for requests - leave as default unless you know you need something else"),
'gpt_model': str,
'temperature': float,
'rate_limit': float
'temperature': (float, "Amount of random variance to add to translations. Generally speaking, none is best"),
'rate_limit': (float, "Maximum OpenAI API requests per minute. Mainly useful if you are on the restricted free plan")
},
'Translation': {
'gpt_prompt': str,
'gpt_prompt': (str, "The (brief) instruction to give GPT for each batch of subtitles. [movie_name] and [to_language] are automatically filled in"),
'target_language': str,
'instruction_file': str,
'allow_retranslations': bool,
'enforce_line_parity': bool
'include_original': (bool, "Include original text in translated subtitles"),
'instruction_file': (str, "Detailed instructions for GPT about how to approach translation, and the required response format"),
'allow_retranslations': (bool, "If true, translations that fail validation will be sent to GPT again with a note to allow it to correct the mistake"),
'enforce_line_parity': (bool, "Validator: If true, require one translated line for each source line")
},
'Advanced': {
'min_batch_size': int,
'max_batch_size': int,
'scene_threshold': float,
'batch_threshold': float,
'match_partial_words': bool,
'whitespaces_to_newline': bool,
'max_context_summaries': int,
'max_characters': int,
'max_newlines': int,
'match_partial_words': (bool, "Used with substitutions, required for some languages where word boundaries aren't detected"),
'whitespaces_to_newline': (bool, "Convert blocks of whitespace and Chinese Commas to newlines"),
'max_context_summaries': (int, "Limits the number of scene/batch summaries to include as context with each translation batch"),
'max_characters': (int, "Validator: Maximum number of characters to allow in a single translated line"),
'max_newlines': (int, "Validator: Maximum number of newlines to allow in a single translated line"),
'max_retries': int,
'backoff_time': float
}
Expand Down Expand Up @@ -82,7 +83,8 @@ def create_section_widget(self, settings, section_name):
layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)

for key, key_type in settings.items():
field = CreateOptionWidget(key, self.options[key], key_type)
key_type, tooltip = key_type if isinstance(key_type, tuple) else (key_type, None)
field = CreateOptionWidget(key, self.options[key], key_type, tooltip=tooltip)
layout.addRow(field.name, field)

return section_widget
Expand Down
47 changes: 28 additions & 19 deletions GUI/Widgets/ProjectOptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging
from PySide6.QtWidgets import QGroupBox, QVBoxLayout, QLabel, QLineEdit, QCheckBox, QPushButton, QDialog
from PySide6.QtCore import Signal
from PySide6.QtCore import Signal, QSignalBlocker
from GUI.TranslatorOptions import TranslatorOptionsDialog

from GUI.Widgets.Widgets import OptionsGrid, TextBoxEditor
Expand All @@ -27,11 +27,12 @@ def __init__(self, options=None):
# Add options
self.AddSingleLineOption(0, "Movie Name", options, 'movie_name')
self.AddSingleLineOption(1, "Target Language", options, 'target_language')
self.AddMultiLineOption(2, "Description", options, 'description')
self.AddMultiLineOption(3, "Characters", options, 'characters')
self.AddMultiLineOption(4, "Substitutions", options, 'substitutions')
self.AddCheckboxOption(5, "Match Partial Words", options, 'match_partial_words')
self.AddButtonOption(6, "", "GPT Settings", self._gpt_settings)
self.AddCheckboxOption(2, "Include Original Text", options, 'include_original')
self.AddMultiLineOption(3, "Description", options, 'description')
self.AddMultiLineOption(4, "Characters", options, 'characters')
self.AddMultiLineOption(5, "Substitutions", options, 'substitutions')
self.AddCheckboxOption(6, "Match Partial Words", options, 'match_partial_words')
self.AddButtonOption(7, "", "GPT Settings", self._gpt_settings)

self.Populate(options)

Expand All @@ -44,6 +45,7 @@ def GetOptions(self):
options = {
"movie_name": self.movie_name_input.text(),
"target_language": self.target_language_input.text(),
"include_original": self.include_original_input.isChecked(),
"description": self.description_input.toPlainText(),
"characters": ParseCharacters(self.characters_input.toPlainText()),
"substitutions": ParseSubstitutions(self.substitutions_input.toPlainText()),
Expand Down Expand Up @@ -83,6 +85,7 @@ def AddCheckboxOption(self, row, label, options, key):
input_widget = QCheckBox(self)
value = options.get(key, False)
input_widget.setChecked(value)
input_widget.stateChanged.connect(self._check_changed)

self.grid_layout.addWidget(label_widget, row, 0)
self.grid_layout.addWidget(input_widget, row, 1)
Expand Down Expand Up @@ -113,22 +116,24 @@ def Populate(self, options):
'scene_threshold' : options.get('scene_threshold'),
}

for key in options:
if hasattr(self, key + "_input"):
value = options.get(key)
self._setvalue(key, value)
with QSignalBlocker(self):
for key in options:
if hasattr(self, key + "_input"):
value = options.get(key)
self._setvalue(key, value)

def Clear(self):
for key in ["movie_name", "description", "characters", "substitutions", "match_partial_words"]:
input = getattr(self, key + "_input")
if input:
if isinstance(input, QCheckBox):
input.setChecked(False)
with QSignalBlocker(self):
for key in ["movie_name", "description", "characters", "substitutions", "match_partial_words", "include_original"]:
input = getattr(self, key + "_input")
if input:
if isinstance(input, QCheckBox):
input.setChecked(False)
else:
input.clear()
else:
input.clear()
else:
logging.error(f"No input found for {key}")

logging.error(f"No input found for {key}")

def _setvalue(self, key, value):
if isinstance(value, bool):
getattr(self, key + "_input").setChecked(value or False)
Expand All @@ -147,6 +152,10 @@ def _text_changed(self, text = None):
options = self.GetOptions()
self.optionsChanged.emit(options)

def _check_changed(self, int = None):
options = self.GetOptions()
self.optionsChanged.emit(options)

def _gpt_settings(self):
dialog = TranslatorOptionsDialog(self._gpt_options, parent=self)
result = dialog.exec()
Expand Down
1 change: 1 addition & 0 deletions PySubtitleGPT/Options.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def env_bool(key, default=False):
'gpt_prompt': os.getenv('GPT_PROMPT', "Please translate these subtitles[ for movie][ to language]."),
'instruction_file': os.getenv('INSTRUCTION_FILE', "instructions.txt"),
'target_language': os.getenv('TARGET_LANGUAGE', 'English'),
'include_original': env_bool('INCLUDE_ORIGINAL', False),
'temperature': float(os.getenv('TEMPERATURE', 0.0)),
'allow_retranslations': env_bool('ALLOW_RETRANSLATIONS', True),
'scene_threshold': float(os.getenv('SCENE_THRESHOLD', 30.0)),
Expand Down
22 changes: 18 additions & 4 deletions PySubtitleGPT/SubtitleFile.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ def SaveOriginals(self, path : str = None):
with open(self.sourcepath, 'w', encoding=default_encoding) as f:
f.write(srtfile)

def SaveTranslation(self, outputpath : str = None):
def SaveTranslation(self, outputpath : str = None, include_original : bool = False):
"""
Write translated subtitles to an SRT file
"""
Expand All @@ -181,6 +181,9 @@ def SaveTranslation(self, outputpath : str = None):
if not translated:
logging.error("No subtitles translated")
return

if include_original:
translated = self._merge_original_and_translated(originals, translated)

logging.info(f"Saving translation to {str(outputpath)}")

Expand Down Expand Up @@ -211,7 +214,8 @@ def UpdateContext(self, options):
'max_batch_size' : None,
'batch_threshold' : None,
'scene_threshold' : None,
'match_partial_words': False
'match_partial_words': False,
'include_original': False
}

with self.lock:
Expand All @@ -226,9 +230,9 @@ def UpdateContext(self, options):

# Update the context dictionary with matching fields from options, and vice versa
for key in context.keys():
if options.get(key):
if key in options:
context[key] = options[key]
elif context[key]:
elif key in context:
options[key] = context[key]

# Fill description from synopsis for backward compatibility
Expand Down Expand Up @@ -414,3 +418,13 @@ def Sanitise(self):
batch.translated = [line for line in batch.translated if line.number and line.text]

self.scenes = [scene for scene in self.scenes if scene.batches]

def _merge_original_and_translated(self, originals : list[SubtitleLine], translated : list[SubtitleLine]):
lines = {item.key: SubtitleLine(item.line) for item in originals if item.key}

for item in translated:
if item.key in lines:
line = lines[item.key]
line.text = f"{line.text}\n{item.text}"

return sorted(lines.values(), key=lambda item: item.key)
18 changes: 12 additions & 6 deletions PySubtitleGPT/SubtitleProject.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,19 +111,26 @@ def TranslateSubtitles(self):

translator.TranslateSubtitles()

self.subtitles.SaveTranslation()
self.SaveSubtitles()

except TranslationAbortedError:
logging.warning(f"Translation aborted")
raise

except Exception as e:
if self.subtitles and self.options.get('stop_on_error'):
self.subtitles.SaveTranslation()
self.SaveSubtitles()

logging.error(f"Failed to translate subtitles")
raise

def SaveSubtitles(self, outputpath : str = None):
"""
Write output file
"""
include_original = self.options.get('include_original', False)
self.subtitles.SaveTranslation(outputpath, include_original=include_original)

def TranslateScene(self, scene_number, batch_numbers = None, translator : SubtitleTranslator = None):
"""
Pass batches of subtitles to the translation engine.
Expand All @@ -142,7 +149,7 @@ def TranslateScene(self, scene_number, batch_numbers = None, translator : Subtit

translator.TranslateScene(scene, batch_numbers=batch_numbers)

self.subtitles.SaveTranslation()
self.SaveSubtitles()

return scene

Expand All @@ -151,7 +158,7 @@ def TranslateScene(self, scene_number, batch_numbers = None, translator : Subtit

except Exception as e:
if self.subtitles and self.options.get('stop_on_error'):
self.subtitles.SaveTranslation()
self.SaveSubtitles()

logging.error(f"Failed to translate subtitles")
raise
Expand Down Expand Up @@ -278,7 +285,6 @@ def UpdateProjectOptions(self, options: dict):
with self.lock:
# Update "self.options"
self.options.update(options)
self.options.Save()

if self.subtitles:
self.subtitles.UpdateContext(self.options)
Expand Down Expand Up @@ -311,6 +317,6 @@ def _on_batch_translated(self, batch):

def _on_scene_translated(self, scene):
logging.debug("Scene translated")
self.subtitles.SaveTranslation()
self.SaveSubtitles()
self.needsupdate = self.update_project
self.events.scene_translated(scene)
2 changes: 2 additions & 0 deletions gpt-subtrans.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
parser.add_argument('--scenethreshold', type=float, default=None, help="Number of seconds between lines to consider a new scene")
parser.add_argument('--maxlines', type=int, default=None, help="Maximum number of lines(subtitles) to process in this run")
parser.add_argument('--matchpartialwords', action='store_true', help="Allow substitutions that do not match not on word boundaries")
parser.add_argument('--includeoriginal', action='store_true', help="Include the original text in the translated subtitles")

args = parser.parse_args()

Expand All @@ -47,6 +48,7 @@
'characters': ParseCharacters(args.characters or args.character),
'substitutions': ParseSubstitutions(args.substitution),
'match_partial_words': args.matchpartialwords,
'include_original': args.includeoriginal,
'instruction_file': args.instructionfile,
'instruction_args': args.instruction,
'min_batch_size': args.minbatchsize,
Expand Down
1 change: 1 addition & 0 deletions gui-subtrans.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def parse_arguments():
parser.add_argument('--maxlines', type=int, default=None, help="Maximum number of batches to process")
parser.add_argument('--theme', type=str, default=None, help="Stylesheet to load")
parser.add_argument('--firstrun', action='store_true', help="Show the first-run options dialog on launch")
parser.add_argument('--includeoriginal', action='store_true', help="Include the original text in the translated subtitles")

try:
args = parser.parse_args()
Expand Down

0 comments on commit 32232a6

Please sign in to comment.