Skip to content

Commit

Permalink
Load/save instructions from app data folder (#188)
Browse files Browse the repository at this point in the history
Check in application resource directory and user data folder (e.g. %appdata%\MachineWrapped\GPTSubtrans\instructions) for instructions files and combine them into one list - user data takes precedence over application resource if the names are the same.

The user data path is used by default for loading and saving instructions files.
  • Loading branch information
machinewrapped authored Jun 29, 2024
1 parent 5916119 commit 0ddc531
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 21 deletions.
31 changes: 27 additions & 4 deletions GUI/EditInstructionsDialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
QVBoxLayout,
QHBoxLayout,
QPushButton,
QComboBox,
QFileDialog,
QSizePolicy
)
from GUI.Widgets.OptionsWidgets import CreateOptionWidget

from PySubtitle.Options import MULTILINE_OPTION, Options
from PySubtitle.Instructions import GetInstructionsResourcePath, Instructions
from PySubtitle.Instructions import Instructions, GetInstructionsFiles, GetInstructionsUserPath, LoadInstructions

class EditInstructionsDialog(QDialog):
def __init__(self, settings : dict, parent=None):
Expand All @@ -25,7 +26,6 @@ def __init__(self, settings : dict, parent=None):
self.target_language = None
self.filters = "Text Files (*.txt);;All Files (*)"


self.form_layout = QFormLayout()
self.prompt_edit = self._add_form_option("prompt", self.instructions.prompt, str, "Prompt for each translation request")
self.instructions_edit = self._add_form_option("instructions", self.instructions.instructions, MULTILINE_OPTION, "System instructions for the translator")
Expand All @@ -34,6 +34,7 @@ def __init__(self, settings : dict, parent=None):

self.button_layout = QHBoxLayout()

self.select_file = self._create_instruction_dropdown(self._select_instructions)
self.load_button = self._create_button("Load Instructions", self._load_instructions)
self.save_button = self._create_button("Save Instructions", self._save_instructions)
self.default_button = self._create_button("Defaults", self.set_defaults)
Expand All @@ -55,6 +56,14 @@ def _add_form_option(self, key, initial_value, key_type, tooltip = None):
self.form_layout.addRow(key, input)
return input

def _create_instruction_dropdown(self, on_change):
instructions_files = GetInstructionsFiles()
initial_value = self.instructions.instruction_file
dropdown = CreateOptionWidget('instruction_file', initial_value, instructions_files)
dropdown.contentChanged.connect(on_change)
self.button_layout.addWidget(dropdown)
return dropdown

def _create_button(self, text, on_click):
button = QPushButton(text)
button.clicked.connect(on_click)
Expand Down Expand Up @@ -92,10 +101,24 @@ def load_icon(self):
def save_icon(self):
return QApplication.style().standardIcon(QStyle.StandardPixmap.SP_DialogSaveButton)

def _select_instructions(self):
'''Select an instruction file from the dropdown'''
instructions_name = self.select_file.GetValue()

try:
self.instructions = LoadInstructions(instructions_name)

self.prompt_edit.SetValue(self.instructions.prompt)
self.instructions_edit.SetValue(self.instructions.instructions)
self.retry_instructions_edit.SetValue(self.instructions.retry_instructions)

except Exception as e:
logging.error(f"Unable to load instructions: {str(e)}")

def _load_instructions(self):
'''Load instructions from a file'''
options = QFileDialog.Options()
path = GetInstructionsResourcePath(self.instructions.instruction_file)
path = GetInstructionsUserPath(self.instructions.instruction_file)
file_name, _ = QFileDialog.getOpenFileName(self, "Load Instructions", dir=path, filter=self.filters, options=options)
if file_name:
try:
Expand All @@ -111,7 +134,7 @@ def _load_instructions(self):
def _save_instructions(self):
'''Save instructions to a file'''
options = QFileDialog.Options()
filepath = GetInstructionsResourcePath(self.instructions.instruction_file)
filepath = GetInstructionsUserPath(self.instructions.instruction_file)
file_name, _ = QFileDialog.getSaveFileName(self, "Save Instructions", dir=filepath, filter=self.filters, options=options)
if file_name:
try:
Expand Down
8 changes: 4 additions & 4 deletions GUI/NewProjectSettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from GUI.ProjectDataModel import ProjectDataModel
from GUI.Widgets.OptionsWidgets import CreateOptionWidget, DropdownOptionWidget

from PySubtitle.Instructions import GetInstructionFiles, LoadInstructionsResource
from PySubtitle.Instructions import GetInstructionsFiles, LoadInstructions
from PySubtitle.SubtitleBatcher import SubtitleBatcher
from PySubtitle.SubtitleLine import SubtitleLine
from PySubtitle.SubtitleProcessor import SubtitleProcessor
Expand Down Expand Up @@ -53,7 +53,7 @@ def __init__(self, datamodel : ProjectDataModel, parent=None):
self.OPTIONS['model'] = (available_models, self.OPTIONS['model'][1])
self.settings['model'] = datamodel.selected_model

instruction_files = GetInstructionFiles()
instruction_files = GetInstructionsFiles()
if instruction_files:
self.OPTIONS['instruction_file'] = (instruction_files, self.OPTIONS['instruction_file'][1])

Expand Down Expand Up @@ -99,7 +99,7 @@ def accept(self):
if instructions_file:
logging.info(f"Project instructions set from {instructions_file}")
try:
instructions = LoadInstructionsResource(instructions_file)
instructions = LoadInstructions(instructions_file)

self.settings['prompt'] = instructions.prompt
self.settings['instructions'] = instructions.instructions
Expand Down Expand Up @@ -151,7 +151,7 @@ def _update_instruction_file(self):
instruction_file = self.fields['instruction_file'].GetValue()
if instruction_file:
try:
instructions = LoadInstructionsResource(instruction_file)
instructions = LoadInstructions(instruction_file)
self.fields['prompt'].SetValue(instructions.prompt)
if instructions.target_language:
self.fields['target_language'].SetValue(instructions.target_language)
Expand Down
6 changes: 3 additions & 3 deletions GUI/SettingsDialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from GUI.GuiHelpers import ClearForm, GetThemeNames

from GUI.Widgets.OptionsWidgets import CreateOptionWidget
from PySubtitle.Instructions import GetInstructionFiles, LoadInstructionsResource
from PySubtitle.Instructions import GetInstructionsFiles, LoadInstructions
from PySubtitle.Options import Options
from PySubtitle.Substitutions import Substitutions
from PySubtitle.TranslationProvider import TranslationProvider
Expand Down Expand Up @@ -115,7 +115,7 @@ def __init__(self, options : Options, provider_cache = None, parent=None, focus_
self.SECTIONS['General']['theme'] = ['default'] + GetThemeNames()

# Query available instruction files
instruction_files = GetInstructionFiles()
instruction_files = GetInstructionsFiles()
if instruction_files:
self.SECTIONS['General']['instruction_file'] = instruction_files

Expand Down Expand Up @@ -371,7 +371,7 @@ def _update_instruction_file(self):
instruction_file = self.widgets['instruction_file'].GetValue()
if instruction_file:
try:
instructions = LoadInstructionsResource(instruction_file)
instructions = LoadInstructions(instruction_file)
self.widgets['prompt'].SetValue(instructions.prompt)
if instructions.target_language:
self.widgets['target_language'].SetValue(instructions.target_language)
Expand Down
3 changes: 3 additions & 0 deletions PySubtitle/Helpers/Resources.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@

import os
import sys
import appdirs

config_dir = appdirs.user_config_dir("GPTSubtrans", "MachineWrapped", roaming=True)

def GetResourcePath(relative_path, *parts):
"""
Expand Down
52 changes: 48 additions & 4 deletions PySubtitle/Instructions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging
import os

from PySubtitle.Helpers.Resources import GetResourcePath
from PySubtitle.Helpers.Resources import GetResourcePath, config_dir

linesep = '\n'

Expand Down Expand Up @@ -86,7 +86,7 @@ def InitialiseInstructions(self, settings : dict):
self.instructions = ReplaceTags(self.instructions, tags)
self.retry_instructions = ReplaceTags(self.retry_instructions, tags)

def LoadInstructionsFile(self, filepath):
def LoadInstructionsFile(self, filepath : str):
"""
Try to load instructions from a text file.
"""
Expand Down Expand Up @@ -125,7 +125,7 @@ def LoadInstructionsFile(self, filepath):
if not self.prompt or not self.instructions:
raise ValueError("Invalid instruction file")

def SaveInstructions(self, filepath):
def SaveInstructions(self, filepath : str):
"""
Save instructions to a text file.
"""
Expand Down Expand Up @@ -155,7 +155,7 @@ def GetInstructionsResourcePath(instructions_file : str = None):

return GetResourcePath("instructions", instructions_file)

def GetInstructionFiles():
def GetInstructionsResourceFiles():
"""
Get a list of instruction files in the instructions directory.
"""
Expand All @@ -174,6 +174,50 @@ def LoadInstructionsResource(resource_name):
instructions.LoadInstructionsFile(filepath)
return instructions

def GetInstructionsUserPath(instructions_file : str = None):
"""
Get the path for an instructions file (or the directory that contains them).
"""
instructions_dir = os.path.join(config_dir, "instructions")
return os.path.join(instructions_dir, instructions_file) if instructions_file else instructions_dir

def GetInstructionsUserFiles():
"""
Get a list of instruction files in the user directory.
"""
instructions_dir = GetInstructionsUserPath()
logging.debug(f"Looking for instruction files in {instructions_dir}")
if not os.path.exists(instructions_dir):
os.makedirs(instructions_dir)

files = os.listdir(instructions_dir)
return [ file for file in files if file.lower().endswith(".txt") ]

def GetInstructionsFiles():
"""
Get a list of instruction files in the user and resource directories.
"""
default_instructions = GetInstructionsResourceFiles()
user_instructions = GetInstructionsUserFiles()

instructions_map = { os.path.basename(file).lower(): file for file in default_instructions }
instructions_map.update({ os.path.basename(file).lower(): file for file in user_instructions })

# Sort 'instructions.txt' to the top of the list followed by other names case-insensitive
return sorted(list(instructions_map.values()), key=lambda x: (x.lower() != 'instructions.txt', x.lower()))

def LoadInstructions(name : str):
"""
Load instructions from user directory if they exist, otherwise load from resources.
"""
user_path = GetInstructionsUserPath(name)
if os.path.exists(user_path):
instructions = Instructions({})
instructions.LoadInstructionsFile(user_path)
return instructions

return LoadInstructionsResource(name)

def LoadLegacyInstructions(lines):
"""
Retry instructions can be added to the file after a line of at least 3 # characters.
Expand Down
7 changes: 3 additions & 4 deletions PySubtitle/Options.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@
import logging
import os
import dotenv
import appdirs

from PySubtitle.Instructions import Instructions, LoadInstructionsResource
from PySubtitle.Instructions import Instructions, LoadInstructions
from PySubtitle.Helpers.Resources import config_dir
from PySubtitle.Helpers.Text import standard_filler_words
from PySubtitle.version import __version__

MULTILINE_OPTION = 'multiline'

config_dir = appdirs.user_config_dir("GPTSubtrans", "MachineWrapped", roaming=True)
settings_path = os.path.join(config_dir, 'settings.json')
default_user_prompt = "Translate these subtitles [ for movie][ to language]"

Expand Down Expand Up @@ -233,7 +232,7 @@ def InitialiseInstructions(self):
instruction_file = self.get('instruction_file')
if instruction_file:
try:
instructions = LoadInstructionsResource(instruction_file)
instructions = LoadInstructions(instruction_file)
self.options['prompt'] = instructions.prompt
self.options['instructions'] = instructions.instructions
self.options['retry_instructions'] = instructions.retry_instructions
Expand Down
3 changes: 1 addition & 2 deletions PySubtitle/VersionCheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
import datetime
import logging
import requests
import appdirs

from PySubtitle.version import __version__
from PySubtitle.Helpers.Resources import config_dir

repo_name = "gpt-subtrans"
repo_owner = "machinewrapped"

config_dir = appdirs.user_config_dir("GPTSubtrans", "MachineWrapped", roaming=True)
last_check_file = os.path.join(config_dir, 'last_check.txt')

def CheckIfUpdateAvailable():
Expand Down

0 comments on commit 0ddc531

Please sign in to comment.