Sintetizar fala em documentos usando Ruby
Para um documento curto, você pode extrair o texto em Ruby e enviá-lo ao Google Cloud Text-to-Speech em uma única solicitação. O exemplo abaixo mostra uma prévia do texto selecionado e salva a narração MP3 em um novo arquivo. Ele aceita texto simples e HTML em UTF-8, com limite de 1 MiB para o arquivo de origem e de 5.000 bytes para o texto extraído.
Configurar o Google Cloud Text-to-Speech
Use GNU/Linux com Ruby e Bundler instalados. Este guia foi testado com Ruby 4.0.7,
Bundler 4.0.23 e o timeout do GNU coreutils; você precisa de um player de MP3 local para conferir o resultado.
Use uma versão do Ruby que ainda receba manutenção, disponível na página de downloads do Ruby.
O comando timeout abaixo depende do GNU coreutils, portanto não é diretamente portável para macOS
ou Windows.
Você também precisa de um projeto do Google Cloud com faturamento e a API Text-to-Speech ativados, além de credenciais com permissão para usá-la. Siga as instruções de configuração do projeto e o guia de Application Default Credentials do Google antes de executar a síntese. O código usa ADC; ele não faz login nem altera sua configuração na nuvem. O texto do documento é enviado ao Google, e as solicitações podem gerar cobranças. Comece com um texto de exemplo sem informações sensíveis.
Crie um diretório de trabalho novo e vazio, abra um terminal Bash nele e salve o conteúdo abaixo como Gemfile.
A fonte de gems explícita permite que o Bundler baixe as dependências:
source 'https://rubygems.org'
gem 'google-cloud-text_to_speech', '2.2.1'
gem 'nokogiri', '1.19.4'
Instale as dependências neste projeto e mantenha o Gemfile.lock resultante junto ao seu código:
bundle config set --local path vendor/bundle &&
bundle install --gemfile ./Gemfile
Script básico de narração de documentos
Salve esta classe como document_narrator.rb. A validação ocorre antes da criação do cliente, portanto a prévia
do texto ou a recusa de um destino existente não exige credenciais da nuvem. O texto extraído deve
conter algo além de espaços em branco e respeitar o
limite de conteúdo de 5.000 bytes do Google.
Por exemplo, 2.501 cópias de é excedem esse limite, embora sejam menos de 5.000 caracteres.
require 'google/cloud/text_to_speech'
require 'nokogiri'
require 'tempfile'
class DocumentNarrator
class InputError < StandardError; end
def initialize(client: nil)
@client = client
end
def extract_text(file_path)
raise DocumentNarrator::InputError, 'Input must be a regular file' unless File.file?(file_path)
source = File.binread(file_path, 1024 * 1024 + 1)
raise DocumentNarrator::InputError, 'Source exceeds 1 MiB' if source.bytesize > 1024 * 1024
source.force_encoding(Encoding::UTF_8)
raise DocumentNarrator::InputError, 'Source must be valid UTF-8' unless source.valid_encoding?
text = case File.extname(file_path).downcase
when '.txt'
source
when '.html', '.htm'
begin
doc = Nokogiri::HTML5(source, nil, 'UTF-8', max_tree_depth: 400, max_attributes: 400)
rescue ArgumentError
raise InputError, 'HTML nesting or attribute limit exceeded; prepare a .txt file'
end
doc.css('script, style, template, noscript').remove
doc.at_css('body').xpath('.//text()').map(&:text).join(' ').strip
else
raise DocumentNarrator::InputError, 'Use a .txt, .html, or .htm input'
end
raise DocumentNarrator::InputError, 'Extracted text is empty' if text.strip.empty?
raise DocumentNarrator::InputError, 'Extracted text exceeds 5,000 bytes' if text.bytesize > 5000
text
end
def narrate_file(input_path, output_path, language_code: 'en-US')
text = extract_text(input_path)
if File.exist?(output_path) || File.symlink?(output_path)
raise DocumentNarrator::InputError, 'Output must not exist; choose a new filename'
end
Tempfile.create(['.narration-', '.mp3'], File.dirname(output_path)) do |file|
file.binmode
client = @client || Google::Cloud::TextToSpeech.text_to_speech
response = client.synthesize_speech(
input: { text: text },
voice: { language_code: language_code, ssml_gender: :FEMALE },
audio_config: { audio_encoding: :MP3 }
)
raise IOError, 'Synthesis returned empty audio' if response.audio_content.empty?
file.write(response.audio_content)
file.flush
# A hard link publishes the complete candidate without replacing another writer's file.
File.link(file.path, output_path)
end
end
end
A extração de HTML lê o texto do corpo na ordem do documento, insere um espaço entre os nós de texto
e remove os elementos script, style, template e noscript. Ela não renderiza CSS, não respeita todos os atributos
que ocultam conteúdo nem reproduz a ordem de leitura acessível de um navegador. Ela também pode
separar uma palavra que se estende por vários elementos inline. Confira a prévia e prepare uma versão
em texto simples quando essa ordem for inadequada.
O parser de HTML também limita a profundidade da árvore DOM a 400
e a quantidade de atributos a 400 por elemento. PDFs e arquivos de processadores de texto precisam
de uma etapa de extração separada e estão fora do escopo deste script.
Recursos avançados e tratamento de erros
Salve este ponto de entrada como narrate.rb ao lado da classe. Os nomes dos arquivos devem ser válidos em UTF-8,
inclusive quando o shell usa a configuração regional POSIX. O delimitador -- distingue os argumentos
de síntese do modo de prévia e permite nomes de arquivo que começam com hífen.
require_relative 'document_narrator'
begin
args = ARGV.map { |arg| arg.dup.force_encoding(Encoding::UTF_8) }
raise DocumentNarrator::InputError, 'Arguments must be valid UTF-8' unless args.all?(&:valid_encoding?)
mode = args.shift
narrator = DocumentNarrator.new
if mode == '--preview' && args.length == 1
puts narrator.extract_text(args[0])
elsif mode == '--' && (2..3).cover?(args.length)
narrator.narrate_file(args[0], args[1], language_code: args[2] || 'en-US')
puts 'Narration saved'
else
raise DocumentNarrator::InputError, 'Usage: narrate.rb --preview INPUT | -- INPUT NEW_OUTPUT [LANGUAGE]'
end
rescue DocumentNarrator::InputError => error
warn error.message
exit 1
rescue StandardError
warn 'Narration failed. Check file permissions, credentials, API access, and your connection.'
exit 1
end
Crie document.html com este pequeno exemplo:
<!doctype html>
<html lang="en">
<head><title>Not spoken</title></head>
<body><p>The copper robot carries a yellow umbrella.</p><script>Not spoken either.</script></body>
</html>
Confira a prévia antes de fazer uma solicitação paga:
bundle exec ruby narrate.rb --preview document.html
A prévia deve conter apenas esta frase, além de possíveis espaços em branco ao redor:
The copper robot carries a yellow umbrella.
Em seguida, sintetize o texto:
timeout --signal=TERM --kill-after=2s 60s \
bundle exec ruby narrate.rb -- document.html narration.mp3 en-US
Isso dá ao comando inteiro 60 segundos, incluindo a inicialização do Ruby, o carregamento das
credenciais ou a renovação OAuth e a síntese. O timeout do GNU permite mais dois segundos antes de encerrar
à força um processo que não responde. Exceder o tempo limite é uma falha, mesmo que o provedor tenha
recebido a solicitação; inspecione o destino antes de tentar novamente. O encerramento forçado pode
deixar um arquivo temporário .narration-.
Verificar a narração e tratar falhas
Em caso de sucesso, o comando imprime Narration saved e cria
narration.mp3. Abra exatamente esse arquivo no seu player de MP3 e ouça até o fim, comparando a narração
com a prévia. Um MP3 que não esteja vazio ou que possa ser reproduzido, por si só, não comprova que o
texto correto foi narrado. A configuração de idioma da voz não traduz o documento; use um código de
idioma que corresponda ao texto.
O diretório pai do destino já deve existir e oferecer suporte a links físicos. Um arquivo ou link simbólico existente é recusado, e a publicação também falha se outro processo criar esse caminho durante a síntese. Uma falha do provedor mantém um destino existente intacto. Escolha outro destino para fazer uma segunda gravação; o script não tem opção de sobrescrita.
Os erros de entrada informam qual verificação falhou. Para um arquivo de origem acima de 1 MiB ou texto extraído acima de 5.000 bytes, prepare um documento menor em vez de truncá-lo silenciosamente. Se houver uma recusa por aninhamento ou atributos de HTML, prepare uma versão em texto simples. Em caso de falha genérica, tente primeiro a prévia e depois verifique as permissões do sistema de arquivos e, no projeto selecionado, as ADC, o faturamento, o acesso à API e a conexão de rede. Os detalhes de erros do provedor e o texto do documento não são incluídos nos diagnósticos de síntese. Uma narração mais longa precisa de um fluxo de trabalho separado para divisão em blocos e montagem; este exemplo faz deliberadamente uma única solicitação síncrona.
