Сохранение озвучки книги средствами Google TTS и python

В последнее время я полюбил слушать аудиокниги. Однако те книги, которые я хочу слушать никто не озвучивает. Не думаю что кому то будет интересна моя драматичная история о выборе лучшего tts, проблемы в процессе написания, солнце в монитор и т.п., так что я просто представлю вам уже готовое решение.

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

Учение о получении токена

Для использования нужен токен. Его можно получить на сайте https://cloud.google.com/text-to-speech/ используя демо форму. Для этого нужно после захода на страницу нажать сочетание клавиш Ctrl+Shift+I. После, в открывшемся меню нажимаете или сразу на вкладку Network, или нажимаете на две стрелочки вправо и выбираете этот пункт там:

Где найти NetworkГде найти Network

После, вам нужно будет на сайте переместиться всего лишь на один экран вниз, где вы нажимаете на синюю кнопку «SPEAK IT». Скорее всего вас попросят решить капчу. Если же вас остановит отладчик, не отчаивайтесь. Достаточно всего лишь в открывшемся окне консоли нажать кнопки в такой последовательности:

image-loader.svg

Когда машина заговорит, нажмите еще раз на ту же синюю кнопку, что бы она заткнулась. Если описанное в абзаце выше произошло и вас перебросило на другую вкладку, возвращаемся на вкладку Network и вводим в поле фильтра, которое активно изначально, фразу «cxl-services» (естественно без кавычек). У вас останется единственный пакет, кликаем на него и сворачиваем в открывшемся поле все лишние заголовки кроме Query String Parameters, что бы не мешали (но вы можете и ручками пролистать до нужного поля). Ищем в Query String Parameters поле token, и копируем его значение.

Отступление. Я бы не стал так извращаться, если бы у меня получилось запустить сервисный аккаунт гугла. Но, к сожалению, для использования озвучки им необходима кредитка и много чего еще. Нет, спасибо.

Токен действует не вечно, через некоторое время его придется заменить, но на озвучку не одной книги должно хватить. Так же предупреждаю, если прогнать слишком много текста, google не будет отвечать некоторое продолжительное время на запросы с вашего ip, так что знайте меру. Куда вводить токен я расскажу далее.

Для работы боту требуется, как не странно, сама книга в текстовом формате без всяких украшений, чистый txt. Моя читалка спокойно позволяет экспортировать книгу в такой формат. Название самой книги задается в конфиге.

Файл настроек

Сам же конфиг должен называться «config.ini» и содержать в себе текст следующего содержания:

config.ini

[book]
token='03AGdBq240zoxyndgTq90pn8syc-cc_ig4qfBBol9_WZBaW9V9NAudiKmWWQJCQ2iaYx9sk5N9NvGTrFSuzRMUV0N0hQTne2laWMuFwWa8Wy_qFgVXk120u5jnJow3Ga9abf_8WfTdFX1x2rXnOzn2xHO_0QvcRnKeJ-NemXFIrn1Whh7JG9xTn3VJ6VmUmOvZV2u8RDnbPcW-K3LN6v_yyRetFs37lbdMIUvJcU_RDt7nI9QiZScH-8i80LkF1EyQFYkrYBh3E5LAuAqsyISL_c60vWZrqV9GBzW_00qI_mzsMb4fLtg_Y8wigMKg_bj2huFI1O3B70feyxuqMtz1iY7LuJYhK8V4haUh76uQZZHnjygL6hlZpS38kPIc9aDB8dA0KFZQoFLPpomYUTZK-GKFQEEcOdmIZ2Pl23pF0OXWKLJAers8sVInCHglYufd8ERYALuRw7zu'
; Ваш токен, который вы можете получить отправив в google tts любой запрос.
speed=0.8
; Скорость, с которой будет читать tts.
directory=Книга N
; Название каталога, в который будут кидаться аудиофайлы.
filename=synthesize-text-audio-
; Название итогового аудиофайла. Учтите, что в конце будет стоять порядковый номер.
threads=5
; Количество потоков. Если вы поставите слишком много, скрипт изменит на максимально комфортное вашему процессору. Да и учитывайте, что google может не одобрить большое количество запросов.
nameBook=text.txt
; Название книги, которая будет озвучиваться.
start=0
; Указывает, с какого порядкового номера скрипту начинать озвучивать. Полезно, например, в случае если вы на середине книги осознали, что ударение на каком то слове неправильное.
audioEncoding=LINEAR16
; Указывает, в какой кодировке сервис будет возвращать озвучку. Идеальный для меня - LINEAR16. В нем отсутствуют какие либо артефакты звучания. Но если для вас трафик или размер итогового файла важен, можете поставить в значение mp3.
name=ru-RU-Wavenet-D
; Указывает, кто должен озвучивать книгу. На сайте https://cloud.google.com/text-to-speech/ можете посмотреть, кто вам больше всего заходит.
glossary=TTS.lexx
; Указывает название словаря, из которого будут парситься исправления ударений.

Все переменные прокомментированы, не думаю что где то возникнут сложности.

Код программы

Без комментариев

import requests
import os
import multiprocessing
import time
import random
import configparser
import base64
import json
configF = configparser.ConfigParser()
configF.read("config.ini")
nameBook=str(configF['book']['nameBook'])
f = open(nameBook)
text = f.read()
f.close()
data = ('{"input":{"text":"'+text+'"},"voice":{"languageCode":"ru-RU","name":"ru-RU-Wavenet-D"},"audioConfig":{"audioEncoding":"LINEAR16","pitch":0,"speakingRate":0.8}}').encode('utf-8')
try:
    f = open(str(configF['book']['glossary']))
    config = f.read()
    f.close()
except:
    print('Отсутствует словарь!')
    config=''
config=config.replace('"','')
config=config.split('=')
zabiv=1
text=text.replace('"',";").replace('.'," .")
print("начинается замена")
while zabiv+1 < len(config):
    text=text.replace(str(config[zabiv].split('\n')[1]), str(config[zabiv+1].split('\n')[0]))
    zabiv=zabiv+1
print("заменено успешно")
text = text.split('\n')
i=0

i=i+1
text2=text[i]
def compil(i,textjs):
    global configF
    direct = str(configF['book']['directory'])
    filename = str(configF['book']['filename'])
    audio_content=textjs
    try:
        f = open(direct+'/'+filename+str(i)+'.mp3','wb')
    except:
        os.mkdir(direct)
        f = open(direct+'/'+filename+str(i)+'.mp3','wb')
    f.write(base64.b64decode(audio_content))
    f.close()

def razbivN(text):
    i=0
    ii=0
    ioi=[0]
    text2=''
    while (len(text)-1) > i:
        try:
            text2=''
            while len(text2+'\n'+str(text[i+1]))<4999:
                i=i+1
                if text2 != str(text[i]):
                    text2=text2+'\n'+str(text[i])
            ioi.append(i)
        except:
            i=i
        ii+=1
    return(ii,ioi)
def razbiv(lii,text):
    i=lii
    text2=''
    try:
        while len(text2+'\n'+str(text[i+1]))<4999:
            i=i+1
            if text2 != str(text[i]):
                text2=text2+'\n'+str(text[i])
    except:
        print('усе')
    return(text2)

def sendText(text2,data,i):
    global configF
    token=str(configF['book']['token'])
    speed=str(configF['book']['speed'])
    headers = {
    'authority': 'cxl-services.appspot.com',
    'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="92"',
    'dnt': '1',
    'sec-ch-ua-mobile': '?0',
    'user-agent': 'Mozilla/5.0',
    'content-type': 'text/plain;charset=UTF-8',
    'accept': '*/*',
    'origin': 'https://www.gstatic.com',
    'sec-fetch-site': 'cross-site',
    'sec-fetch-mode': 'cors',
    'sec-fetch-dest': 'empty',
    'referer': 'https://www.gstatic.com/',
    'accept-language': 'ru-RU,ru;q=0.9',
    }
    params = (
    ('url', 'https://texttospeech.googleapis.com/v1beta1/text:synthesize'),
    ('token', token),
    )
    error=1
    while error!=0:
        data = ('{"input":{"text":"'+str(text2)+'"},"voice":{"languageCode":"ru-RU","name":"'+str(configF['book']['name'])+'"},"audioConfig":{"audioEncoding":"'+str(configF['book']['audioEncoding'])+'","pitch":0,"speakingRate":'+speed+'}}').encode('utf-8')
        response = requests.post('http://cxl-services.appspot.com/proxy', headers=headers, params=params, data=data)
        response=response.text.encode().decode()
        error=0
        if response != '' and response !=' Service Unavailable' and response != 'Service Unavailable' and response != 'Unauthorized':
            response=json.loads(response)["audioContent"]
            error=0
        else:
            print('Произошло что то не то. Скорее всего у вас устарел токен.')
            print(response)
            time.sleep(60)
            error+=1
        if len(response)<200:
            print(response)
            print(len(str(text2)))
            error+=1
        else:
            print('успешно '+str(i))
            compil(i,response)
            return(i,text2)
def osnov(text,trii,lli):
    text2=razbiv(lli[trii],text)
    sendTex = sendText(text2,data,trii)
    i=sendTex[0]
    text2=sendTex[1]
    return(i)

ii=razbivN(text)
lli=ii[1]
ii=int(ii[0])
trii=int(configF['book']['start'])
boolThreads=int(configF['book']['threads'])
if boolThreads > int(multiprocessing.cpu_count()):
    boolThreads = int(multiprocessing.cpu_count())
    print('вы выставили слишком большое кол-во потоков, ваш пк не поддерживает столько.')
while trii < ii:
    if len(multiprocessing.active_children()) < boolThreads:
        print(str(trii)+'('+str((trii*100)//ii)+'%)')
        my_thread = multiprocessing.Process(target=osnov, args=(text,trii,lli,))
        my_thread.start()
        trii+=1
        time.sleep(1)

Так же прилагаю словарь ударений, позаимствованный с 4pda для одной из модификаций google tts. С некоторыми моими дополнениями. Небольшой неожиданный камень в огород хабра — почему нельзя загрузить файлы напрямую в статью? Почему я должен извращаться с загрузкой файла на сторонние сервисы? Ладно.

Продолжим. Все четыре файла должны лежать в одной директории со скриптом примерно так:

image-loader.svg

Если у вас что то не заработало, отправьте лог, возможно я исправлю. Использовался python 3.9.7.

© Habrahabr.ru