Skip to content

Commit

Permalink
Optimised SubtitleListModel (#67)
Browse files Browse the repository at this point in the history
* Cache first and last line numbers

Saves nearly 20 seconds on a 2000 line translated file!

* Cache size hints

Bucket lines according to a height approximation and cache the expected size. Saves 3+ seconds on a 2000 line translated file.

* Use map to look up row number

Not measured, but an obvious choice
  • Loading branch information
machinewrapped authored Sep 2, 2023
1 parent 9701b18 commit 8b6786b
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 17 deletions.
14 changes: 14 additions & 0 deletions GUI/GuiHelpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,17 @@ def GetInstructionFiles():
instruction_path = GetResourcePath("")
files = os.listdir(instruction_path)
return [ file for file in files if file.lower().startswith("instructions") ]

def GetLineHeight(text: str, wrap_length: int = 60) -> int:
"""
Calculate the number of lines for a given text with wrapping and newline characters.
:param text: The input text.
:param wrap_length: The maximum number of characters per line.
:return: The total number of lines.
"""
if not text:
return 0

wraps = -(-len(text) // wrap_length) if wrap_length else None # Ceiling division
return text.count('\n') + wraps
37 changes: 29 additions & 8 deletions GUI/ProjectViewModel.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from PySide6.QtCore import Qt, QModelIndex

from PySide6.QtGui import QStandardItemModel, QStandardItem
from GUI.GuiHelpers import GetLineHeight
from PySubtitleGPT import SubtitleLine

from PySubtitleGPT.Helpers import FormatMessages, Linearise, UpdateFields
Expand Down Expand Up @@ -85,10 +86,9 @@ def GetLineItem(self, line_number, get_translated):

if batch_item.last_line_number >= line_number:
lines = batch_item.translated if get_translated else batch_item.originals
if lines:
for line_item in lines.values():
if line_item.number == line_number:
return line_item
line = lines.get(line_number, None) if lines else None
if line:
return line

def GetBatchNumbers(self):
"""
Expand Down Expand Up @@ -421,6 +421,7 @@ def __init__(self, is_translation, line_number, model):
self.is_translation = is_translation
self.number = line_number
self.line_model = model
self.height = GetLineHeight(model.get('text'))

self.setData(self.line_model, Qt.ItemDataRole.UserRole)

Expand All @@ -433,6 +434,8 @@ def Update(self, line_update):
if line_update.get('number'):
self.number = line_update['number']

self.height = GetLineHeight(self.line_model.get('text'))

self.setData(self.line_model, Qt.ItemDataRole.UserRole)

def __str__(self) -> str:
Expand Down Expand Up @@ -477,6 +480,10 @@ def __init__(self, scene_number, batch : SubtitleBatch):
'errors': self._get_errors(batch.errors)
}

# cache on demand
self._first_line_num = None
self._last_line_num = None

if batch.translation and os.environ.get("DEBUG_MODE") == "1":
self.batch_model.update({
'response': batch.translation.text,
Expand All @@ -485,7 +492,6 @@ def __init__(self, scene_number, batch : SubtitleBatch):
if batch.translation.prompt:
self.batch_model['messages'] = FormatMessages(batch.translation.prompt.messages)


self.setData(self.batch_model, Qt.ItemDataRole.UserRole)

def AddLineItem(self, is_translation : bool, line_number : int, model : dict):
Expand All @@ -497,6 +503,8 @@ def AddLineItem(self, is_translation : bool, line_number : int, model : dict):
else:
self.originals[line_number] = line_item

self._invalidate_first_and_last()

@property
def original_count(self):
return len(self.originals)
Expand Down Expand Up @@ -531,11 +539,15 @@ def response(self):

@property
def first_line_number(self):
return min(num for num in self.originals.keys() if num) if self.originals else None
if not self._first_line_num:
self._update_first_and_last()
return self._first_line_num

@property
def last_line_number(self):
return max(num for num in self.originals.keys() if num) if self.originals else None
if not self._last_line_num:
self._update_first_and_last()
return self._last_line_num

@property
def has_errors(self):
Expand Down Expand Up @@ -575,7 +587,16 @@ def GetContent(self):
'errors' : self.has_errors
}
}


def _update_first_and_last(self):
line_numbers = [ num for num in self.originals.keys() if num ] if self.originals else None
self._first_line_num = min(line_numbers) if line_numbers else None
self._last_line_num = max(line_numbers)

def _invalidate_first_and_last(self):
self._first_line_num = None
self._last_line_num = None

def _get_errors(self, errors):
if errors:
if all(isinstance(e, Exception) for e in errors):
Expand Down
22 changes: 16 additions & 6 deletions GUI/SubtitleListModel.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from PySide6.QtCore import QAbstractProxyModel, QModelIndex, Qt
from PySide6.QtCore import QAbstractProxyModel, QModelIndex, Qt, QSize
from GUI.ProjectViewModel import BatchItem, ProjectViewModel, SceneItem, LineItem, ViewModelItem
from GUI.ProjectSelection import ProjectSelection
from GUI.Widgets.Widgets import LineItemView
Expand All @@ -11,6 +11,8 @@ def __init__(self, show_translated, viewmodel=None, parent=None):
self.viewmodel : ProjectViewModel = viewmodel
self.selected_batch_numbers = []
self.visible = []
self.visible_row_map : dict(int, int) = {}
self.size_map : dict(int, QSize ) = {}

# Connect signals to update mapping when source model changes
if self.viewmodel:
Expand Down Expand Up @@ -50,16 +52,16 @@ def ShowSelectedBatches(self, batch_numbers):
visible.extend(visible_lines)

self.visible = visible
self.visible_row_map = { item[2] : row for row, item in enumerate(self.visible) }
self.layoutChanged.emit()

def mapFromSource(self, source_index : QModelIndex):
item : ViewModelItem = self.viewmodel.itemFromIndex(source_index)

if isinstance(item, LineItem):
for row, key in enumerate(self.visible):
scene, batch, line = key
if line == item.number:
return self.index(row, 0, QModelIndex())
row = self.visible_row_map.get(item.number, None)
if row is not None:
return self.index(row, 0, QModelIndex())

return QModelIndex()

Expand Down Expand Up @@ -144,7 +146,15 @@ def data(self, index, role):
return LineItemView(item)

if role == Qt.ItemDataRole.SizeHintRole:
return LineItemView(item).sizeHint()
if not item.height:
return LineItemView(item).sizeHint()

if item.height in self.size_map:
return self.size_map[item.height]

size = LineItemView(item).sizeHint()
self.size_map[item.height] = size
return size

return None

Expand Down
30 changes: 27 additions & 3 deletions gui-subtrans.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
import logging
import os
import sys
import cProfile
from pstats import Stats

from PySide6.QtWidgets import QApplication
from GUI.MainWindow import MainWindow
from PySubtitleGPT.Options import Options, settings_path
from PySubtitleGPT.Options import Options, settings_path, config_dir


# This seems insane but ChatGPT told me to do it.
project_dir = os.path.abspath(os.path.dirname(__file__))
Expand All @@ -31,6 +34,7 @@ def parse_arguments():
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")
parser.add_argument('--profile', action='store_true', help="Profile execution and write stats to the console")

try:
args = parser.parse_args()
Expand All @@ -50,11 +54,28 @@ def parse_arguments():
'scene_threshold': args.scenethreshold,
'project': args.project and args.project.lower() or 'true',
'theme': args.theme,
'firstrun': args.firstrun
'firstrun': args.firstrun,
'profile': args.profile
}

return arguments, args.filepath

def run_with_profiler(app):
profiler = cProfile.Profile()
profiler.enable()

app.exec()

profiler.disable()

profile_path = os.path.join(config_dir, 'profile_guisubtrans.txt')
with open(profile_path, 'w') as stream:
stats = Stats(profiler, stream=stream)
stats.sort_stats('tottime')
stats.print_stats(100)

logging.info(f"Profiling stats written to {profile_path}")

if __name__ == "__main__":
app = QApplication(sys.argv)

Expand All @@ -71,4 +92,7 @@ def parse_arguments():
app.main_window = MainWindow( options=options, filepath=filepath)
app.main_window.show()

app.exec()
if arguments.get('profile'):
run_with_profiler(app)
else:
app.exec()

0 comments on commit 8b6786b

Please sign in to comment.