Tool A simple translator using a pre-trained AI model and PyQt.

wsnlndr

Member
May 20, 2023
126
503
Here I have been playing around with ChatGPT to get information about how to use a model to translate at home, without having to resort to online translators.

What I have discovered, and it is not surprising, is that to use an AI model and make it work quickly, you need to have powerful hardware resources.

In this case, the tool is an interface to translate text from English to Spanish.
You can always use an appropriate model to translate into another source and destination language or even use a large model that contains many possibilities in a single model.

The ideal is to use Cuda cores of the GPU to speed up the process since the CPU is not usually the most optimal for this task, but if the graphics card you have is not powerful, the process will not be fast to translate a simple line of text , but if you have more than one graphics card the process can be significantly accelerated with the appropriate code adjustments.

You can get the models for free at
The interface is as simple as possible using PyQt6 on GNU/Linux.
It would be ideal to automate the translation process to apply it to batch files.

Source code:
You don't have permission to view the spoiler content. Log in or register now.


Captura desde 2024-06-19 07-45-18.png
 

wsnlndr

Member
May 20, 2023
126
503
To use a translator like this for Renpy or other type projects, it may be interesting to implement the use of rules to avoid unpleasant surprises in the processing of the text that is translated and also improve the interface by giving more configuration options.

Example of filter lines of text by rules:
Python:
def apply_rules(words, rules):
    total_words = 0
    total_characters = 0

    # Count the total number of words and characters before applying the rules
    for rule in rules:
        if rule == "Count Words":
            total_words = len(" ".join(words).split())
        if rule == "Count Characters":
            total_characters = len("".join(words))

    if total_words > 0:
        print(f'Total number of words: {total_words}')
    
    if total_characters > 0:
        print(f'Total number of characters: {total_characters}')

    processed_words = []

    for word in words:
        for rule in rules:
            if rule.startswith('Remove Spaces'):
                word = word.replace(' ', '')
            elif rule.startswith('Convert to Lowercase'):
                word = word.lower()
            elif rule.startswith('Convert to Uppercase'):
                word = word.upper()
            elif rule.startswith('Remove Punctuation'):
                if '(' in rule and ')' in rule:
                    parameter = rule[rule.index('(') + 1:rule.index(')')]
                    word = remove_punctuation(word, parameter)
            elif rule.startswith('Remove Final Period'):
                word = word.rstrip('.')
            elif rule.startswith('Remove Empty Lines'):
                if not word.strip():
                    word = ''
            elif rule.startswith('Remove Numbers'):
                word = ''.join(filter(lambda x: not x.isdigit(), word))
            elif rule.startswith('Replace Characters'):
                if '(' in rule and ')' in rule:
                    parameters = rule[rule.index('(') + 1:rule.index(')')].split(',')
                    if len(parameters) == 2:
                        word = word.replace(parameters[0].strip(), parameters[1].strip())
            elif rule.startswith('Remove Short Words'):
                if '(' in rule and ')' in rule:
                    length = int(rule[rule.index('(') + 1:rule.index(')')])
                    if len(word) < length:
                        word = ''
            elif rule.startswith('Remove Long Words'):
                if '(' in rule and ')' in rule:
                    length = int(rule[rule.index('(') + 1:rule.index(')')])
                    if len(word) > length:
                        word = ''
            elif rule.startswith('Remove Specific Words'):
                if '(' in rule and ')' in rule:
                    specific_words = rule[rule.index('(') + 1:rule.index(')')].split(',')
                    if word in specific_words:
                        word = ''
            elif rule.startswith('Keep Only Letters'):
                word = ''.join(filter(lambda x: x.isalpha(), word))
            elif rule.startswith('Reverse Text'):
                word = word[::-1]
            elif rule.startswith('Sort Alphabetically'):
                word = ''.join(sorted(word))
            elif rule.startswith('Add Prefix'):
                if '(' in rule and ')' in rule:
                    prefix = rule[rule.index('(') + 1:rule.index(')')]
                    word = f'{prefix}{word}'
            elif rule.startswith('Add Suffix'):
                if '(' in rule and ')' in rule:
                    suffix = rule[rule.index('(') + 1:rule.index(')')]
                    word = f'{word}{suffix}'
            elif rule.startswith('Remove Words with Special Characters'):
                word = ''.join(filter(lambda x: x.isalnum(), word))
            elif rule.startswith('Capitalize Words'):
                word = word.capitalize()

        if word:
            processed_words.append(word)
 
Last edited:

wsnlndr

Member
May 20, 2023
126
503
Some parameters have been added here to fine-tune the model's work a bit, there are probably better ways to do this,
either by training the model better with a larger amount of data or by configuring its parameters for heavier work,
but I'll leave that to whoever has better hardware.

You don't have permission to view the spoiler content. Log in or register now.


Captura desde 2024-06-23 18-59-04.png
 

wsnlndr

Member
May 20, 2023
126
503
And here we have an updated version of the simple translator, this is now ->Translateador de la pradera to pofesioná, V 0.0003 :D

In order to use the program you must meet some requirements from pip:
huggingface-hub
transformers
langdetect
pyqt6
torch

In the installation of the model, some more things, some modules necessary to be able to work with CUDA will be installed, some are a little heavy but they are necessary.
In this case an Nvidia card is being used, I don't know if the same python modules or different ones are used to work with AMD, I haven't investigated that, sorry!

Each individual model can weigh more or less on the hard drive, in my case I seem to remember that the model I am using weighs about 315 Mb, there are much heavier models, but those require a lot of GPU power and are usually used in data centers.

This program uses the GPU, the more powerful the graphics card = the faster the program will work, you can also use the CPU but unless you have a threadripper, I would use the GPU and its CUDA cores.

Monitor the temperature of your GPU!
This is a recommendation from those who usually play with AI, if they say it for a reason it must be, right?

When you have installed the model correctly and all the necessary modules, you can run the translation program, keep in mind that this program does not work fast, at least with my gtx 1070, it is not fast, maybe if you have a 3080 or higher, in In your case it may not be so slow, but this program cannot be compared with the speed of translators like Paloslios_official.

This program does not use cloud services or online translators, so you do not need any tokens or anything similar, once everything is installed, the program can work perfectly without an internet connection.

There are a series of buttons available that I am going to explain to clarify doubts:

Select folder -> will try to translate all rpy within a folder.
Load *.rpy, -> it will translate only the rpy that we select.
Translate -> Start the translation. Stop translation -> stop translation.
Close -> closes the program.
Detect -> detects the rpy, rpa, rpyc of a folder and shows them in the list...This function is not very useful, I think.
Unzip uses the system decompressor (GNU/Linux) to decompress a game.
Delete-RpyC -> Delete the selected rpyc files.
UnRpa -> makes use of unrpa which must be previously installed from pip ---->
UnRpyC-> makes use of unrpyc which must be previously installed from its repository at ---->

Something important to keep in mind, this program is useful for translations from the extraction of those made with Renpy, those that we do with the Renpy SDK or also translations of rpy files focused on the text and not on the game mechanics , because if the mix of text and game mechanics are very mixed up, the translation errors will be too many, enough to not want to use this translation, so you have to keep in mind that some files like screens are better not to translate. The extraction of translations "with Renpy" in this case must be done manually with the SDK, the program does not have that functionality at the moment.

Conclusion: 99 percent of the time I use the Paloslios translator, because it is fast as thunder and effective, but it can rarely fail, especially because it is a program for Windows that is running with Wine on Linux and because Sometimes some novel and game Devs don't have time to edit several files and instead do their ["magic"] in a single line, forgetting the good programming practices of the Renpy SDK.
(Be careful, I'm not complaining, it just happens sometimes.)

So this program has helped me to be able to rarely translate my favorite novel, the resulting translation is not usually free of errors, some line that is not translated, some poorly translated line or some line that blocks the start of the game or novel, those I try to correct it by hand.

And finally, this program can be improved a lot, I'm sure, after all, I'm not a programmer, just an amateur. If the program helps you or you make a better implementation of it, it will have been worth the time to post the code! All the best.

Captura desde 2024-07-14 01-49-39.png

A screenshot of a game in which I use this program.

Captura desde 2024-07-14 01-57-09.png

Here the source code of the model installer and the translator.

I use few models from -> , for En-Es, Ru-Es, Pt-Es, etc...


You don't have permission to view the spoiler content. Log in or register now.

View attachment translateador.zip
 
Last edited:
  • Like
Reactions: Paloslios_Official

Paloslios_Official

Member
Modder
Apr 14, 2024
384
5,203
The extraction of translations "with Renpy" in this case must be done manually with the SDK, the program does not have that functionality at the moment.
I give you a clue?
I'm sure you can understand it
;)

Bash:
E:\MyLoyalPets-1.0-SE-pc\lib\py3-windows-x86_64/python.exe E:\MyLoyalPets-1.0-SE-pc\MyLoyalPets.py --empty E:\MyLoyalPets-1.0-SE-pc translate spanish
extraer_traducciones.zip
 
  • Heart
Reactions: wsnlndr

wsnlndr

Member
May 20, 2023
126
503
And here we have an extractor that in principle worked excellent, I have to test it more exhaustively, but for now it does not disappoint,
this is Paloslios' contribution so that Gnu&Linux users can make native translations.

The only thing you have to do is run the python script, select the base folder of the game or novel and in a short time we will have a "/game/tl/spanish" folder or any other language with the rpy files and their lines that will later be translated by the...
Translateador de la pradera to pofesioná V-0.0003. :D

The fact is that the way to translate extraction rpy files in the tl/idioma_any/ folder is ideal for the translator based on a pre-trained model, since its implementation of this was focused on this translation mode,
despite the fact that part of the code has other paths, so now with the extractor, probably some things from the translator are superfluous.
In any case, there should not be any big problems using the extractor as a basis for work.

For the game or novel to use the translation that we have added, we must indicate its in the code, I think it is case sensitive, so be careful with uppercase and lowercase letters.

Python:
init python:

    config.default_language ="spanish"
    config.language ="spanish"

Mil gracias Paloslios por la ayuda. == Thank you very much Paloslios for the help.
Python:
for i in range(1000):
    print("Gracias Paloslios")
Source code of the extractor:

Python:
#!/usr/bin/python
import sys
import os
import subprocess
from PyQt6.QtWidgets import QApplication, QFileDialog

def select_game_folder():
    app = QApplication(sys.argv)
    folder = QFileDialog.getExistingDirectory(None, "Select the game folder")
    if folder:
        return folder
    else:
        print("No folder selected.")
        sys.exit(1)

def find_python_executable(base_path):
    potential_dirs = ["py3-linux-x86_64", "py2-linux-i686"]
    for dir_name in potential_dirs:
        full_path = os.path.join(base_path, "lib", dir_name, "python")
        if os.path.isfile(full_path):
            return full_path
    print("Python executable not found.")
    sys.exit(1)

def find_game_script(base_path):
    py_files = [f for f in os.listdir(base_path) if f.endswith(".py")]
    if len(py_files) == 1:
        return os.path.join(base_path, py_files[0])
    elif len(py_files) > 1:
        print("Multiple .py files found. Unable to determine the correct game script.")
        sys.exit(1)
    else:
        print("No .py file found in the game folder.")
        sys.exit(1)

def main():
    game_folder = select_game_folder()
    python_executable = find_python_executable(game_folder)
    game_script = find_game_script(game_folder)

    translate_folder = os.path.join(game_folder, "translate", "spanish")
    os.makedirs(translate_folder, exist_ok=True)

    command = [
        python_executable,
        game_script,
        game_folder,
        "translate",
        "spanish"
    ]

    subprocess.run(command)

if __name__ == "__main__":
    main()
 
Last edited:
  • Haha
Reactions: Paloslios_Official

randysum

Newbie
Oct 5, 2021
19
42


Has some good comparison's of the LLM's their ability to translate and their ability to be explicit.
 

wsnlndr

Member
May 20, 2023
126
503


Has some good comparison's of the LLM's their ability to translate and their ability to be explicit.
@randysum Thanks for the link! (y)

These python codes above to have a translator at home that works in offline mode are good as an emergency tool for Ren'py games, it is good if the connection is not stable for some reason, also as a small project to experiment and even learn it can be useful.

But the reality is that a lot of GPU power is needed for the translation speed to be attractive, today we want everything done in 1 second and in this case this method of translation, fails miserably in speed (my PC & GPU).

On the other hand, the quality of the Helsinki-NLP models is high and are trained only for specialized translation for a source language and a target language, if the job is done well, I prefer a ~400Mb model to a ~4.7Gb (7B) In my case my GPU cries at 7B

Of course, everything will depend on how powerful the GPU in question is for off line mode.
 
  • Like
Reactions: randysum

wsnlndr

Member
May 20, 2023
126
503
Update of the extractor (focus on Linux).

Python:
#!/usr/bin/python

import os
import sys
import subprocess
import shutil
import tempfile
from PyQt6.QtWidgets import QApplication, QFileDialog
import difflib
import stat

# Variable global para controlar los mensajes de depuración
debug_mode = False  # Cambiar a False para desactivar los mensajes de depuración

def debug_print(message):
    """
    Función para imprimir mensajes de depuración solo si el modo debug está activado.
    """
    if debug_mode:
        print(f"[DEBUG] {message}")

class CarpetaTemporal:
    @staticmethod
    def crear_carpeta_temporal(prefijo="temp_"):
        """
        Crea una carpeta temporal en el sistema y devuelve su ruta.
        """
        ruta_temporal = tempfile.mkdtemp(prefix=prefijo)
        debug_print(f"Carpeta temporal creada: {ruta_temporal}")
        return ruta_temporal

    @staticmethod
    def eliminar_carpeta(carpeta):
        """
        Elimina una carpeta y todo su contenido de forma recursiva.
        """
        if os.path.exists(carpeta):
            shutil.rmtree(carpeta)
            debug_print(f"Carpeta eliminada: {carpeta}")
        else:
            debug_print(f"La carpeta no existe: {carpeta}")


class Extractor:
    @staticmethod
    def select_game_folder():
        """
        Abre un cuadro de diálogo para seleccionar la carpeta del juego.
        """
        debug_print("[DEBUG] Abriendo cuadro de diálogo para seleccionar carpeta del juego.")
        app = QApplication(sys.argv)
        folder = QFileDialog.getExistingDirectory(None, "Select the game folder")
        if folder:
            debug_print(f"Carpeta seleccionada: {folder}")
            return folder
        else:
            print("[ERROR] Ninguna carpeta seleccionada.")
            sys.exit(1)

    @staticmethod
    def find_python_executable(base_path):
        """
        Busca el ejecutable de Python en los directorios potenciales y verifica permisos.
        """
        debug_print(f"Buscando ejecutable de Python en la carpeta base: {base_path}")
        potential_dirs = ["linux-x86_64", "linux-i686", "py3-linux-x86_64", "py2-linux-i686"]
        for dir_name in potential_dirs:
            full_path = os.path.join(base_path, "lib", dir_name, "python")
            debug_print(f"Comprobando ruta: {full_path}")
            if os.path.isfile(full_path):
                debug_print(f"Ejecutable encontrado: {full_path}")
                # Verificar si el archivo es ejecutable
                if not os.access(full_path, os.X_OK):
                    debug_print(f"El ejecutable {full_path} no tiene permisos de ejecución. Asignando permisos...")
                    # Aplicar permisos de ejecución
                    os.chmod(full_path, os.stat(full_path).st_mode | stat.S_IXUSR)
                    debug_print(f"Permisos asignados a {full_path}")
                return full_path
        print("[ERROR] No se encontró el ejecutable de Python.")
        sys.exit(1)

    @staticmethod
    def find_game_script(base_path):
        """
        Busca el script del juego en la carpeta base, comparando su nombre con el de la carpeta.
        """
        debug_print(f"Buscando el script del juego en la carpeta base: {base_path}")
        # Obtener nombre base de la carpeta
        folder_name = os.path.basename(base_path).split('_')[0]
        debug_print(f"Nombre base de la carpeta: {folder_name}")

        # Obtener todos los archivos .py en la carpeta
        py_files = [f for f in os.listdir(base_path) if f.endswith(".py")]
        debug_print(f"Archivos .py encontrados: {py_files}")

        if not py_files:
            print("[ERROR] No se encontraron archivos .py en la carpeta.")
            sys.exit(1)

        # Buscar el archivo python más similar al nombre de la carpeta del juego de turno.
        most_similar_file = max(py_files, key=lambda f: difflib.SequenceMatcher(None, folder_name, f).ratio())
        similarity_score = difflib.SequenceMatcher(None, folder_name, most_similar_file).ratio()
        debug_print(f"Archivo más similar: {most_similar_file} (similaridad: {similarity_score})")

        if similarity_score < 0.5:  # Ajusta este umbral según tus necesidades
            print(f"[ERROR] No se encontró un archivo .py adecuado. Mejor coincidencia: {most_similar_file}")
            sys.exit(1)

        return os.path.join(base_path, most_similar_file)


def main():
    """
    Función principal que utiliza la clase Extractor para gestionar los scripts del juego.
    """
    debug_print("Inicio del programa.")
    game_folder = Extractor.select_game_folder()
    debug_print(f"Carpeta del juego: {game_folder}")

    python_executable = Extractor.find_python_executable(game_folder)
    debug_print(f"Ejecutable de Python: {python_executable}")

    game_script = Extractor.find_game_script(game_folder)
    debug_print(f"Script del juego: {game_script}")

    # Configurar la ruta de extracción correcta
    translate_folder = os.path.join(game_folder, "game",  "tl", "spanish")
    debug_print(f"Creando carpeta de traducción en: {translate_folder}")
    os.makedirs(translate_folder, exist_ok=True)

    command = [
        python_executable,
        game_script,
        game_folder,
        "translate",
        "spanish"
    ]

    debug_print(f"Ejecutando comando: {command}")
    subprocess.run(command)
    print("Hecho!")
    debug_print("Fin del programa.")


if __name__ == "__main__":
    main()
 
  • Like
Reactions: Paloslios_Official

wsnlndr

Member
May 20, 2023
126
503
Another version of the simple translator OffLine.
In this case, this is more minimalist, some people say less is more and that is the attempt in this program, to make it simpler.


Developed to work on Linux, as always there are prerequisites for its operation, some python modules to work with translation models based on MarianMT, Torch, Transformers, huggingface, etc.

PyQt6 for the interface, signals and several other things.
In this case I use a translation model from English to Spanish, but there are many more possibilities as mentioned in previous posts, these models can be fine-tuned to improve them and add more vocabulary focused on translation, it is not complex to do that, nor does it require great computing power as can be expected in training a model from scratch.
In addition, automatic controls are added to unpack Rpa and decompile Rpyc, both are modules that must be installed on the system, I installed unrpa using pip and unrpyc using the github repository, then you just have to add the PATH, so that the system recognizes them as commands and they are available in the terminal.

As always, this is a simple and basic version of a translation program that is not fully automatic and does not cover all the possibilities between different Ren'py-based projects, sometimes you will have to do something manually, and the model translations are not always perfect either, you have to expect errors.

In the code, you can change a value {self.idioma_actual = "es" # Idioma por defecto} on line 165 of main.py to change the language of the buttons, at the moment there are only Spanish and Simplified Chinese (request from a friend), but you can add any other language to the buttons in the config.json file if you wish.

The way it works is as simple as the program itself, click on select game folder, click on unzip rpa (the usual), click on extract text, click on translate text.
When finished, a folder will have been created inside /game/tl/"language" with the translated rpy, then you will have to add a file for the game to use that translation in /game folder, the name of this file it's not important, but we will use the .rpy extension so that the game can read the file properly.

The code is based on the use of an Nvidia card, it is important to have the Nvidia graphics drivers installed with CUDA support to speed up the translation methods, otherwise it will work with CPU and the speed will probably drop.


lang.rpy ->
Python:
init 999 python:

    # /game/tl/YourLang
    config.language = "YourLang"

main.py ->
Python:
import os
import subprocess
import json
import shutil  # Importar el módulo shutil para mover archivos de una manera muy shutil!
from PyQt6.QtWidgets import (
    QApplication,
    QMainWindow,
    QVBoxLayout,
    QHBoxLayout,
    QPushButton,
    QTextEdit,
    QFileDialog,
    QWidget, QLabel,
    QProgressBar,
    QMessageBox
)
from PyQt6.QtCore import Qt, QThread, pyqtSignal
from traduccion import Traductor  # Importar el módulo de traducción para que la traducción traducione los archivos traducionados.

def crear_directorio_eco():
    """ Crea el directorio ECO si no existe, iste, iste, iste, te, te, e, e. """
    eco_dir = os.path.join(os.path.dirname(__file__), "ECO")
    if not os.path.exists(eco_dir):
        os.makedirs(eco_dir)
    return eco_dir

class HiloTraduccion(QThread):
    # Señal para enviar el progreso de la traducción
    progreso = pyqtSignal(int)
    # Señal para enviar el texto traducido
    texto_traducido = pyqtSignal(str)
    # Señal para indicar que la traducción ha terminado
    terminado = pyqtSignal()

    def __init__(self, traductor, translation_folder, rpy_files, log_signal):
        super().__init__()
        self.traductor = traductor
        self.translation_folder = translation_folder
        self.rpy_files = rpy_files
        self.log_signal = log_signal  # Señal para enviar logs a la interfaz

    def run(self):
        """
        Método que se ejecuta cuando el hilo comienza, luego lo enrollamos y hacemos un nudo.
        """
        # Procesar cada archivo .rpy
        for rpy_file in self.rpy_files:
            rpy_path = os.path.join(self.translation_folder, rpy_file)
            self.traducir_archivo_rpy(rpy_path)

        # Emitir señal de finalización
        self.terminado.emit()

    def traducir_archivo_rpy(self, rpy_path):
        """
        Traduce el contenido de un archivo .rpy respetando su estructura alias indentación.
        """
        try:
            self.log_signal.emit(f"Iniciando la traducción del archivo: {rpy_path}")
            with open(rpy_path, 'r', encoding='utf-8') as infile:
                lines = infile.readlines()

            translated_lines = []
            translate_block = False
            line_count = 0

            for line in lines:
                if line.startswith('translate '):
                    translate_block = True
                    line_count = 0
                    translated_lines.append(line)
                elif translate_block and line_count == 3:  # AHORA ES 3
                    try:
                        # Extraer el texto entre comillas
                        start = line.find('"') + 1
                        end = line.rfind('"')
                        text_to_translate = line[start:end]

                        # Verificar que haya texto para traducir
                        if start >= end:
                            self.log_signal.emit(f"Advertencia: No se encontró texto para traducir en la línea: {line.strip()}")
                            translated_lines.append(line)
                            continue

                        # Traducir el texto
                        translated_text = self.traductor.traducir_texto(text_to_translate)

                        # Verificar que la traducción no esté vacía
                        if not translated_text:
                            self.log_signal.emit(f"Advertencia: Traducción vacía para el texto: {text_to_translate}")
                            translated_lines.append(line)
                            continue

                        # Reemplazar el texto original con el texto traducido
                        translated_line = line[:start] + translated_text + line[end:]
                        translated_lines.append(translated_line)
                        self.log_signal.emit(f"Traducido: {text_to_translate} -> {translated_text}")
                    except Exception as e:
                        self.log_signal.emit(f"Error al traducir la línea: {line.strip()} - {e}")
                        translated_lines.append(line)  # Mantener la línea original en caso de error
                    translate_block = False  # Reiniciar el bloque de traducción
                elif line.startswith('new "'):
                    try:
                        # Extraer el texto entre comillas
                        start = line.find('"') + 1
                        end = line.rfind('"')
                        text_to_translate = line[start:end]

                        # Verificar que haya texto para traducir
                        if start >= end:
                            self.log_signal.emit(f"Advertencia: No se encontró texto para traducir en la línea: {line.strip()}")
                            translated_lines.append(line)
                            continue

                        # Traducir el texto
                        translated_text = self.traductor.traducir_texto(text_to_translate)

                        # Verificar que la traducción no esté vacía
                        if not translated_text:
                            self.log_signal.emit(f"Advertencia: Traducción vacía para el texto: {text_to_translate}")
                            translated_lines.append(line)
                            continue

                        # Reemplazar el texto original con el texto traducido
                        translated_line = line[:start] + translated_text + line[end:]
                        translated_lines.append(translated_line)
                        self.log_signal.emit(f"Traducido: {text_to_translate} -> {translated_text}")
                    except Exception as e:
                        self.log_signal.emit(f"Error al traducir la línea: {line.strip()} - {e}")
                        translated_lines.append(line)  # Mantener la línea original en caso de error
                else:
                    translated_lines.append(line)

                if translate_block:
                    line_count += 1

            # Escribir el contenido traducido de vuelta al archivo
            try:
                with open(rpy_path, 'w', encoding='utf-8') as outfile:
                    outfile.writelines(translated_lines)
                self.log_signal.emit(f"Archivo traducido con éxito: {rpy_path}")
            except Exception as e:
                self.log_signal.emit(f"Error al escribir en el archivo {rpy_path}: {e}")

        except Exception as e:
            self.log_signal.emit(f"Error al traducir el archivo {rpy_path}: {e}")

class RenPyTranslatorApp(QMainWindow):
    # Señal para enviar mensajes al log
    log_signal = pyqtSignal(str)

    def __init__(self):
        super().__init__()
        self.cargar_configuracion()  # Cargar configuración desde JSON
        self.initUI()
        self.traductor = Traductor()  # Instanciar el traductor
        self.log_signal.connect(self.log)  # Conectar la señal al método log

    def cargar_configuracion(self):
        """
        Carga la configuración desde el archivo JSON sin llegar al rin.
        """
        with open('config.json', 'r', encoding='utf-8') as f:
            self.config = json.load(f)
        self.idioma_actual = "es"  # Idioma por defecto

    def initUI(self):
        # Aplicar el tema
        self.setStyleSheet(f"""
            QMainWindow {{
                background-color: {self.config['theme']['background']};
                color: {self.config['theme']['text_color']};
            }}
            QLabel {{
                font-size: 14px;
                font-weight: bold;
                color: {self.config['theme']['text_color']};
            }}
            QPushButton {{
                background-color: {self.config['theme']['button_background']};
                color: white;
                font-size: 12px;
                padding: 8px;
                border-radius: 5px;
            }}
            QPushButton:hover {{
                background-color: {self.config['theme']['button_hover']};
            }}
            QProgressBar {{
                border: 1px solid #333;
                border-radius: 5px;
                text-align: center;
                background-color: {self.config['theme']['progress_bar_background']};
                color: {self.config['theme']['text_color']};
            }}
            QProgressBar::chunk {{
                background-color: {self.config['theme']['progress_bar_chunk']};
            }}
            QTextEdit {{
                background-color: {self.config['theme']['text_edit_background']};
                color: {self.config['theme']['text_color']};
                border: 1px solid {self.config['theme']['text_edit_border']};
                border-radius: 5px;
                padding: 10px;
            }}
        """)

        # Layout principal (vertical)
        layout_principal = QVBoxLayout()

        # Etiqueta para mostrar la carpeta seleccionada
        self.label_carpeta = QLabel(self.config['texts'][self.idioma_actual]['folder_label'])
        layout_principal.addWidget(self.label_carpeta)

        # Layout horizontal para los botones
        layout_botones = QHBoxLayout()

        # Botones con textos cargados desde JSON
        self.boton_seleccionar = QPushButton(self.config['texts'][self.idioma_actual]['select_folder'])
        self.boton_seleccionar.clicked.connect(self.seleccionar_carpeta)
        layout_botones.addWidget(self.boton_seleccionar)

        self.boton_descomprimir = QPushButton(self.config['texts'][self.idioma_actual]['decompress_rpa'])
        self.boton_descomprimir.clicked.connect(self.descomprimir_rpa)
        layout_botones.addWidget(self.boton_descomprimir)

        self.boton_decompilar = QPushButton(self.config['texts'][self.idioma_actual]['decompile_rpyc'])
        self.boton_decompilar.clicked.connect(self.decompilar_rpyc)
        layout_botones.addWidget(self.boton_decompilar)

        self.boton_extraer = QPushButton(self.config['texts'][self.idioma_actual]['extract_text'])
        self.boton_extraer.clicked.connect(self.extraer_texto)
        layout_botones.addWidget(self.boton_extraer)

        self.boton_traducir = QPushButton(self.config['texts'][self.idioma_actual]['translate_text'])
        self.boton_traducir.clicked.connect(self.iniciar_traduccion)
        layout_botones.addWidget(self.boton_traducir)

        # Añadir el layout de botones al layout principal
        layout_principal.addLayout(layout_botones)

        # Barra de progreso
        self.barra_progreso = QProgressBar()
        layout_principal.addWidget(self.barra_progreso)

        # Área de texto para mostrar logs
        self.texto_log = QTextEdit()
        self.texto_log.setReadOnly(True)
        layout_principal.addWidget(self.texto_log)

        # Contenedor principal
        contenedor = QWidget()
        contenedor.setLayout(layout_principal)
        self.setCentralWidget(contenedor)

        # Establecer el título de la ventana
        self.setWindowTitle(self.config['texts'][self.idioma_actual]['window_title'])

    def seleccionar_carpeta(self):
        """
        Abre un diálogo para seleccionar la carpeta del juego.
        """
        carpeta = QFileDialog.getExistingDirectory(self, "Seleccionar Carpeta del Juego")
        if carpeta:
            self.carpeta_juego = carpeta
            self.label_carpeta.setText(
                self.config['texts'][self.idioma_actual]['log_messages']['folder_selected'].format(folder=carpeta)
            )
            self.log(self.config['texts'][self.idioma_actual]['log_messages']['folder_selected'].format(folder=carpeta))
            self.contar_archivos()  # Llamar a contar_archivos después de seleccionar la carpeta

    def contar_archivos(self):
        """
        Cuenta los archivos .rpa, .rpy y .rpyc en la carpeta del juego y muestra la información en el log.
        """
        if hasattr(self, 'carpeta_juego'):
            rpa_count = 0
            rpy_count = 0
            rpyc_count = 0
            for root, _, files in os.walk(self.carpeta_juego):
                for file in files:
                    if file.endswith('.rpa'):
                        rpa_count += 1
                    elif file.endswith('.rpy'):
                        rpy_count += 1
                    elif file.endswith('.rpyc'):
                        rpyc_count += 1

            self.log(f"Archivos .rpa encontrados: {rpa_count}")
            self.log(f"Archivos .rpy encontrados: {rpy_count}")
            self.log(f"Archivos .rpyc encontrados: {rpyc_count}")
        else:
            self.log(self.config['texts'][self.idioma_actual]['log_messages']['error_no_folder'])

    def descomprimir_rpa(self):
        """
        Descomprime archivos .rpa usando unrpa y mueve los archivos .rpy a la carpeta /game.
        """
        if hasattr(self, 'carpeta_juego'):
            eco_dir = crear_directorio_eco()  # Crear o verificar el directorio ECO
            game_folder = os.path.join(self.carpeta_juego, "game")  # Ruta a la carpeta /game

            # Descomprimir archivos .rpa
            for root, _, files in os.walk(self.carpeta_juego):
                for file in files:
                    if file.endswith('.rpa'):
                        ruta_rpa = os.path.join(root, file)
                        self.log(f"Descomprimiendo {ruta_rpa}...")
                        # Descomprimir en el directorio ECO
                        subprocess.run(['unrpa', '-mp', eco_dir, ruta_rpa])
                        self.log(f"{ruta_rpa} descomprimido en {eco_dir}.")

            # Mover archivos .rpy a la carpeta /game
            rpy_files = [f for f in os.listdir(eco_dir) if f.endswith('.rpy')]
            if rpy_files:
                for file in rpy_files:
                    ruta_rpy = os.path.join(eco_dir, file)
                    ruta_destino = os.path.join(game_folder, file)
                    try:
                        shutil.move(ruta_rpy, ruta_destino)  # Mover el archivo
                        self.log(f"Archivo {file} movido a {game_folder}.")
                    except Exception as e:
                        self.log(f"Error al mover {file}: {e}")
            else:
                self.log("No se encontraron archivos .rpy en el directorio ECO.")

            self.log("Descompresión de archivos .rpa y movimiento de archivos .rpy completados.")  # Mensaje de finalización
        else:
            self.log(self.config['texts'][self.idioma_actual]['log_messages']['error_no_folder'])

    def decompilar_rpyc(self):
        """
        Decompila archivos .rpyc usando unrpyc.
        """
        eco_dir = crear_directorio_eco()  # Crear o verificar el directorio ECO
        rpyc_files = [f for f in os.listdir(eco_dir) if f.endswith('.rpyc')]
        if rpyc_files:
            for file in rpyc_files:
                ruta_rpyc = os.path.join(eco_dir, file)
                self.log(f"Decompilando {ruta_rpyc}...")
                try:
                    subprocess.run(['unrpyc', ruta_rpyc])
                    self.log(f"{ruta_rpyc} decompilado.")
                except FileNotFoundError:
                    self.log("Error: unrpyc no se encuentra. Asegúrate de que esté instalado y en la ruta del sistema.")
                    return
        else:
            self.log("No se encontraron archivos .rpyc en el directorio ECO.")

    def find_python_executable(self, base_path):
        """
        Busca el ejecutable de Python en los directorios potenciales.
        """
        potential_dirs = ["linux-x86_64", "linux-i686", "py3-linux-x86_64", "py2-linux-i686"]
        for dir_name in potential_dirs:
            full_path = os.path.join(base_path, "lib", dir_name, "python")
            if os.path.isfile(full_path):
                return full_path
        self.log("No se encuentra un directorio con el ejecutabre python de turno.")
        return None

    def find_game_script(self, base_path):
        """
        Busca el script del juego (.py) en la carpeta base.
        """
        py_files = [f for f in os.listdir(base_path) if f.endswith(".py")]
        if len(py_files) == 1:
            return os.path.join(base_path, py_files[0])
        elif len(py_files) > 1:
            self.log("Multiple .py files found. Unable to determine the correct game script.")
            return None
        else:
            self.log("No .py file found in the game folder.")
            return None

    def extraer_texto(self):
        """
        Extrae texto utilizando el script del juego.
        """
        if hasattr(self, 'carpeta_juego'):
            # Encontrar el ejecutable de Python y el script del juego
            python_executable = self.find_python_executable(self.carpeta_juego)
            game_script = self.find_game_script(self.carpeta_juego)

            if python_executable and game_script:
                # Configurar la ruta de traducción
                translate_folder = os.path.join(self.carpeta_juego, "translate", "spanish")
                os.makedirs(translate_folder, exist_ok=True)

                # Ejecutar el script del juego con los argumentos de traducción
                command = [
                    python_executable,
                    game_script,
                    self.carpeta_juego,
                    "translate",
                    "spanish"
                ]

                self.log(f"Ejecutando comando: {command}")
                subprocess.run(command)
                self.log("Extracción de texto completada.")
            else:
                self.log("No se pudo encontrar el ejecutable de Python o el script del juego.")
        else:
            self.log(self.config['texts'][self.idioma_actual]['log_messages']['error_no_folder'])

    def iniciar_traduccion(self):
        """
        Inicia la traducción de los archivos .rpy en la carpeta seleccionada/game/tl/spanish/.
        """
        if hasattr(self, 'carpeta_juego'):
            # Ruta a la carpeta /game/tl/spanish
            translation_folder = os.path.join(self.carpeta_juego, "game", "tl", "spanish")

            # Verificar si la carpeta existe
            if not os.path.exists(translation_folder):
                self.log(f"Error: No se encontró la carpeta {translation_folder}.")
                return

            # Obtener la lista de archivos .rpy en la carpeta
            rpy_files = [f for f in os.listdir(translation_folder) if f.endswith('.rpy')]

            if not rpy_files:
                self.log("No se encontraron archivos .rpy en la carpeta de traducción.")
                return

            # Deshabilitar el botón de traducción mientras se procesa
            self.boton_traducir.setEnabled(False)
            self.log("Iniciando la traducción de archivos .rpy...")  # Mensaje de inicio

            # Crear una instancia de HiloTraduccion
            self.hilo_traduccion = HiloTraduccion(self.traductor, translation_folder, rpy_files, self.log_signal)

            # Conectar la señal terminado del hilo a la función traduccion_completada
            self.hilo_traduccion.terminado.connect(self.traduccion_completada)

            # Iniciar el hilo
            self.hilo_traduccion.start()
        else:
            self.log(self.config['texts'][self.idioma_actual]['log_messages']['error_no_folder'])

    def actualizar_progreso(self, valor):
        """
        Actualiza la barra de progreso.
        """
        self.barra_progreso.setValue(valor)

    def mostrar_texto_traducido(self, texto):
        """
        Muestra el texto traducido en el área de logs.
        """
        self.texto_log.append(f"Texto traducido: {texto}")

    def traduccion_completada(self):
        """
        Se llama cuando la traducción ha terminado.
        """
        self.log("Traducción completada.")
        self.boton_traducir.setEnabled(True)  # Habilitar el botón de nuevo

    def log(self, mensaje):
        """
        Muestra un mensaje en el área de texto de logs.
        """
        self.texto_log.append(mensaje)

if __name__ == "__main__":
    app = QApplication([])
    ventana = RenPyTranslatorApp()
    ventana.show()
    app.exec()
traduccion.py ->
Python:
from transformers import MarianTokenizer, MarianMTModel
import torch

class Traductor:
    def __init__(self, model_name='Helsinki-NLP/opus-mt-en-es'):
        """
        Inicializa el modelo de traducción.
        """
        try:
            # Cargar el tokenizador y el modelo
            self.tokenizer = MarianTokenizer.from_pretrained(model_name)
            self.model = MarianMTModel.from_pretrained(model_name)

            # Verificar si hay una GPU disponible
            self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
            self.model.to(self.device)  # Mover el modelo a la GPU (si está disponible)

            # Mensaje de depuración para mostrar el dispositivo en uso
            print(f"Usando dispositivo: {self.device}")
        except Exception as e:
            print(f"Error al inicializar el traductor: {e}")
            raise

    def traducir_texto(self, texto):
        """
        Traduce un texto de inglés a español usando el modelo Opus, peeeeeeero, si cambias el modelo por otro, podras traducir a otros idiomashhh.
        """
        try:
            # Tokenizar el texto usando el método __call__
            tokenized_text = self.tokenizer(texto, return_tensors='pt', padding=True, truncation=True)
            tokenized_text = {k: v.to(self.device) for k, v in tokenized_text.items()}  # Mover datos a la GPU
            # Traducir
            with torch.no_grad():
                translated = self.model.generate(**tokenized_text, max_length=512)  # Limitar la longitud máxima
            # Decodificar el texto traducido
            texto_traducido = self.tokenizer.decode(translated[0], skip_special_tokens=True)
            return texto_traducido
        except Exception as e:
            print(f"Error al traducir el texto: {e}")
            return ""  # Devolver una cadena vacía en caso de error

    def traducir_lote(self, textos):
        """
        Traduce un lote de textos de inglés a español usando el modelo Opus con bastante garbo y salero.
        """
        try:
            # Tokenizar el lote de textos
            tokenized_text = self.tokenizer(textos, return_tensors='pt', padding=True, truncation=True)
            tokenized_text = {k: v.to(self.device) for k, v in tokenized_text.items()}  # Mover datos a la GPU
            with torch.no_grad():
                translated = self.model.generate(**tokenized_text, max_length=512)  # Limitar la longitud máxima
            # Decodificar el lote de textos traducidos
            textos_traducidos = [self.tokenizer.decode(t, skip_special_tokens=True) for t in translated]
            return textos_traducidos
        except Exception as e:
            print(f"Error al traducir el lote de textos: {e}")
            return [""] * len(textos)  # Devolver una lista de cadenas vacías en caso de error
config.json ->
JSON:
{
    "theme": {
        "background": "#2E3440",
        "text_color": "#ECEFF4",
        "button_background": "#4CAF50",
        "button_hover": "#45a049",
        "progress_bar_background": "#3B4252",
        "progress_bar_chunk": "#4CAF50",
        "text_edit_background": "#3B4252",
        "text_edit_border": "#4C566A"
    },
    "texts": {
        "es": {
            "window_title": "Ren'Py Translator",
            "select_folder": "Seleccionar Carpeta",
            "decompress_rpa": "Descomprimir .rpa",
            "decompile_rpyc": "Decompilar .rpyc",
            "extract_text": "Extraer Texto",
            "translate_text": "Traducir Texto",
            "folder_label": "Carpeta del juego no seleccionada",
            "log_messages": {
                "folder_selected": "Carpeta del juego seleccionada: {folder}",
                "rpa_decompressed": "{file} descomprimido.",
                "rpyc_decompiled": "{file} decompilado.",
                "text_extracted": "Texto extraído de {file}.",
                "translation_complete": "Traducción completada.",
                "error_no_folder": "Error: No se ha seleccionado una carpeta del juego.",
                "error_no_text": "Error: No se ha extraído texto para traducir."
            }
        },
        "zh": {
            "window_title": "Ren'Py 翻译器",
            "select_folder": "选择文件夹",
            "decompress_rpa": "解压缩 .rpa",
            "decompile_rpyc": "反编译 .rpyc",
            "extract_text": "提取文本",
            "translate_text": "翻译文本",
            "folder_label": "未选择游戏文件夹",
            "log_messages": {
                "folder_selected": "已选择游戏文件夹: {folder}",
                "rpa_decompressed": "{file} 已解压缩.",
                "rpyc_decompiled": "{file} 已反编译.",
                "text_extracted": "已从 {file} 提取文本.",
                "translation_complete": "翻译完成.",
                "error_no_folder": "错误: 未选择游戏文件夹.",
                "error_no_text": "错误: 未提取文本以供翻译."
            }
        }
    }
}
b.png a.png

There are many things missing and many things that can be improved, but as I'm not good at programming, this is what there is, just a good intention so that others can take advantage of the code and improve it if they want, then they can do whatever they want with the result, obfuscate it or release it as is, that no longer depends on me.

On the other hand, there are some very interesting models that with few computational resources serve to create images like this on your PC with few resources.
With an 8GB VRam graphics card, you can create images like this in less than 1 minute.
Sin título.jpg
Made in easy-diffusion = Lora + CotrolNet + Codeformer + Realesgram + stable-diffusion 1.5 in home! :HideThePain:
 
Last edited: