Как правильно перевести игру “The Lamplighters League”

На данном примере предлагается разобрать перевод .wem файлов аудио одного языка (англ.) на другой язык (русский) с последующей упаковкой в .wem и использования в игре. В качестве инструментов будут использоваться python, нейросети, а также программа Wwise. Из интересного — также будет использоваться сеть, определяющая пол (gender) говорящего, чтобы перевод получился двухголосым.

Алгоритм работы будет следующим :

  • распаковать .wem файлы игры в .wav
  • конвертировать .wav в .txt на английском языке
  • из англ. .wav определить пол говорящего и создать файл-таблицу
  • перевести англ. .txt в .txt на русском
  • конвертировать .txt на рус. в .wav на рус.
  • запаковать готовые .wav обратно в .wem

*- найти пустые файлы c len(text)==0 и перенести их «как есть», т.к. это файлы с воплями, шумами и т.п.
Выглядит просто, посмотрим подробнее.

Распаковать .wem файлы игры в .wav


Подавляющее большинство аудиофайлов игры упакованы в формат wem и находятся в папке с игрой.
Поэтому, первым шагом будет их распаковка для последующей обработки.

И здесь необходимо упомянуть статью, т.к. основной объем информации был получен из нее.

Необходимо скачать саму программу под windows — vgmstream-win64
Создать в ней две папки: input, output и положить в первую все .wem файлы из папки игры из

The Lamplighters League\LamplightersLeague_Data\StreamingAssets\Audio\GeneratedSoundBanks\Windows\English(US).

Далее необходимо создать в папке с программой .bat файл, который обработает все файлы в папке input, т.к. по умолчанию программа обрабатывает только один файл.

Содержимое .bat файла (например ):

for %%f in (input/*.wem) do "./vgmstream-cli.exe" -o output/%%f.wav input/%%f
pause

Так как файлов достаточно много (~15000), процесс займет некоторое время.
На выходе получатся .wav файлы:


Сразу необходимо оговориться, что запаковать обратно в .wem этой же программой не получится.

Движемся дальше.

Конвертировать .wav в .txt на английском языке.


В предыдущей статье при переводе из англ. wav в англ. .txt использовалась vosk-model-en-us-0.22-lgraph, которая неплохо справилась с задачей.

В этот раз попробуем другой «пакет помощи» — wisper
Работает не хуже, и, по ощущениям, качественней.

Пропустим через код все файлы, которые ранее конвертировали:

import whisper

##model = whisper.load_model("base")
##result = model.transcribe("59364.wem.wav")
##print(result["text"])


import torch, time, whisper, os
from glob import glob

device = torch.device('cuda')
model = whisper.load_model("base")
##model = whisper.load_model(name="base", device=device)

##
def job(file):
##    audio = whisper.load_audio(file, sr=16000)
    result = model.transcribe(file,language="en", task='transcribe', fp16=True)
    #print(result['text'])
    a=file.split('\\')[1].split('.')[0]    
    with open (f"{a}.txt","a") as f:
        f.write(result['text'])
##    print(datetime.now()- start)

import multiprocessing
def main():
    p = multiprocessing.Pool(8)
    for f in glob(r'.\*.wav'):
        #job(f)
        #print(f)
        #print('work on:',f.split('\\')[1])
        p.apply_async(job, [f]) 

    p.close()
    p.join() 

if __name__ == '__main__':
    main() 

Используется base модель, которая будет загружена при старте скрипта, cuda. Если нет установленной cuda, можно заменить cpu.

Результатом работы программы будут все те же ~15000 файлов, но уже с англ. текстом:

Необходимо помнить про пустые файлы и файлы с аномалиями в тексте. Пример последних:


We're going to be going to be going to be going to be going to be going to be going to be going to be going to be going to be going ...

Так переведен моделью свист — wav

Wav-файлы на англ., которые соответствуют по именам пустым файлам .txt необходимо перенести «как есть».
Найти пустые файлы можно, например, так:

from glob import glob
for f in glob(r'./*.txt'):
    with open (f) as f2:
        text=f2.read()
    if len(text)==0:
        print(f,text)

Из англ. .wav определить пол говорящего и создать файл-таблицу.

Чтобы получился двухголосый перевод и персонажи не говорили голосами женщин за мужчин и наоборот, определим для каждого wav на англ. пол говорящего.
В самой игре больше двух персонажей, но, в данном случае, это необходимое упрощение.

Для gender-recognition воспользуемся следующим репозиторием.
С данным пакетом будут небольшие сложности при установке, поэтому, рекомендуется взглянуть на issue, чтобы «заставить работать», если не ставите пакет из requirements.txt.

Поместим в папки males и females англ. wav файлы не важно в какой пропорции, программе нужны обе папки (предполагается что вы ранее обучили модели gmm согласно описаниям в репозитории). Также GenderIdentifier.py перед запуском в Voice-based-gender-recognition\Code немного поправим:

GenderIdentifier.py


import os
import pickle
import warnings
import numpy as np
from FeaturesExtractor import FeaturesExtractor

warnings.filterwarnings("ignore")


class GenderIdentifier:

    def __init__(self, females_files_path, males_files_path, females_model_path, males_model_path):
        self.females_training_path = females_files_path
        self.males_training_path   = males_files_path
        self.error                 = 0
        self.total_sample          = 0
        self.features_extractor    = FeaturesExtractor()
        # load models
        self.females_gmm = pickle.load(open(females_model_path, 'rb'))
        self.males_gmm   = pickle.load(open(males_model_path, 'rb'))

    def process(self):
        files = self.get_file_paths(self.females_training_path, self.males_training_path)
        # read the test directory and get the list of test audio files
        for file in files:
            self.total_sample += 1
            #print("%10s %8s %1s" % ("--> TESTING", ":", os.path.basename(file)))
            
            vector = self.features_extractor.extract_features(file)
            winner = self.identify_gender(vector)
            #print(file)
            expected_gender = file.split("\\")[1][:-1]
            #expected_gender = file.split("/")[1][:-1]

            #print("%10s %6s %1s" % ("+ EXPECTATION",":", expected_gender))
            #print("%10s %3s %1s" %  ("+ IDENTIFICATION", ":", winner))
            #print(os.path.basename(file),winner)
            with open ('out.txt','a') as f:
                f.write(f'{os.path.basename(file)},{winner}\n')

            if winner != expected_gender: self.error += 1
            #print("----------------------------------------------------")

        #accuracy     = ( float(self.total_sample - self.error) / float(self.total_sample) ) * 100
        #accuracy_msg = "*** Accuracy = " + str(round(accuracy, 3)) + "% ***"
        #print(accuracy_msg)

    def get_file_paths(self, females_training_path, males_training_path):
        # get file paths
        females = [ os.path.join(females_training_path, f) for f in os.listdir(females_training_path) ]
        males   = [ os.path.join(males_training_path, f) for f in os.listdir(males_training_path) ]
        files   = females + males
        return files

    def identify_gender(self, vector):
        # female hypothesis scoring
        is_female_scores         = np.array(self.females_gmm.score(vector))
        is_female_log_likelihood = is_female_scores.sum()
        # male hypothesis scoring
        is_male_scores         = np.array(self.males_gmm.score(vector))
        is_male_log_likelihood = is_male_scores.sum()

        #print("%10s %5s %1s" % ("+ FEMALE SCORE",":", str(round(is_female_log_likelihood, 3))))
        #print("%10s %7s %1s" % ("+ MALE SCORE", ":", str(round(is_male_log_likelihood,3))))

        if is_male_log_likelihood > is_female_log_likelihood: winner = "male"
        else                                                : winner = "female"
        return winner


if __name__== "__main__":
    #gender_identifier = GenderIdentifier("TestingData/females", "TestingData/males", "females.gmm", "males.gmm")
    #gender_identifier = GenderIdentifier("males", "females", "males.gmm","females.gmm")
    gender_identifier = GenderIdentifier("males", "females", "males.gmm","females.gmm")
    gender_identifier.process()

На выходе получаем файл, который содержит информацию о том, где говорящий — мужчина, где — женщина — file

Что дальше?

Перевести англ. .txt в .txt на русском.

Для перевода текстов будет использоваться тот же подход, что и в предыдущей статье. Без изменений.
На выходе 15k файлов в формате ru:

С текстом на русском языке.

Конвертировать .txt на рус. в .wav на рус.

При переводе из txt в wav опять же будут использоваться silero модели, работа с которыми также была описана в предыдущей статье.
Но с небольшими изменениями: будем учитывать пол говорящего.
Для этого будет использоваться файл out.txt, в котором к именам файлов «привязан» пол. Женский голос — ‘baya’, мужской — ‘eugene’.


import torch,csv
from datetime import datetime
from glob import glob
import multiprocessing
torch._C._jit_set_profiling_mode(False)

language="ru"
model_id = 'v4_ru'
device = torch.device('cpu') 
model, _ = torch.hub.load(repo_or_dir="snakers4/silero-models",
                                     model="silero_tts",
                                     language=language,
                                     speaker=model_id)
model.to(device)  # gpu or cpu
sample_rate = 48000
#speaker="baya" #xenia
put_accent=True
put_yo=True

with open('out.txt') as f:
    genders = {line[0]:line[1] for line in csv.reader(f)}

def job(file):    
    with open (file) as f:
        text=f.read()    
    a=file.split('\\')[1].split('.')[0]    
    gender=genders[f'{a}.wem.wav']
    if gender=='female':
        try:
            model.save_wav(text=text,
                       audio_path= f'{a}.wav',
                       sample_rate = sample_rate,
                       speaker="baya" )
        except:
            pass
    else:
        try:
            model.save_wav(text=text,
                       audio_path= f'{a}.wav',
                       sample_rate = sample_rate,
                       speaker="eugene" )
        except:
            pass

for f in glob(r'./*.ru'):
    job(f)

Результат — все те же 15000 файлов, но уже с русскими мужским и женским голосами.
Процесс локализации игры приблизился к финалу — осталось только запаковать в wem формат готовые файлы.
Но тут не все так просто.

Wav-to-wem.

Wem формат — это просто сжатые wav, если упрощенно. Готовые wem занимают гораздо меньше места на диске, и в этом чем-то похожи на формат ogg.
На github имеется пара-тройка конверторов на python в wem, но с усеченным функционалом. Добиться от них работоспособности под текущую задачу автору не удалось.

Поэтому понадобится программа Wwise от Audiokinetic —

Установить программу — отдельное испытание на стойкость.
Сначала необходимо зарегистрироваться на сайте, а далее для российской публики — ход закрыт.
Скачать загрузчик не получится. VPN? Возможно.
Но только VPN не для браузера, а для ПК. Так как при установке загрузчик будет еще и саму Wwise скачивать.

Если установка успешна — добро пожаловать в Wwise:

Далее, короткий ролик с ресурса — wav-to-wem подкупает своей простотой.
Но… если просто поместить в wwise уже готовые wav на русском и сконвертировать «как есть», готовые wem в игре не заработают. Будет просто тишина, игра при этом вылетать не будет.

Поэтому, необходимо обратить внимание на упаковку первоначальных wem на англ. языке.
Если воспользоваться консольной утилитой vgm-stream из пакета vgmstream-win64 из начала статьи, то можно узнать, что wem-файл на англ. (оригинал) выглядит так:

В то время как WWise по умолчанию выдает следующий формат:

Разница в енкодере и битрейте.

Как это исправить?

После импорта всех аудиофайлов в проект, необходимо выбрать один из них (изменение применятся ко всем остальным автоматически).

Выбрать формат Vorbis, далее нажать Edit во вкладке Adv.:

Выбрать битрейт, приближенный к оригинальному wem на англ.:

Далее, можно конвертировать в wem по стандартной процедуре (см. ролик с youtube выше):

После этого готовыми файлами wem нужно заменить оригиналы в соответствующей папке игры.

На этом процесс перевода можно считать оконченным.
В игре также есть файлы формата .bnk — это, очевидно, файлы диалогов и роликов.
Но этих файлов не так много, и можно «потерпеть английский».

 

Источник

Lamplighters, League, игру, Как, перевести, правильно

Читайте также