Sintetiza voz en documentos con Ruby
Para un documento corto, puedes extraer su texto con Ruby y enviarlo a Google Cloud Text-to-Speech en una sola solicitud. El siguiente ejemplo muestra una vista previa del texto seleccionado y guarda la narración MP3 en un archivo nuevo. Acepta texto sin formato y HTML en UTF-8, con un límite de 1 MiB para el documento de origen y de 5.000 bytes para el texto extraído.
Configura Google Cloud Text-to-Speech
Usa GNU/Linux con Ruby y Bundler instalados. Este tutorial se ha probado con Ruby 4.0.7,
Bundler 4.0.23 y timeout de GNU coreutils; necesitas un reproductor MP3 local para comprobar el resultado.
Usa una versión de Ruby con mantenimiento activo de la página de descargas de Ruby.
El comando timeout que aparece más adelante depende de GNU coreutils, por lo que no es directamente
portable a macOS ni a Windows.
También necesitas un proyecto de Google Cloud con la facturación y la API de Text-to-Speech habilitadas, además de credenciales con permiso para usarla. Sigue la configuración del proyecto de Google y la guía de Application Default Credentials antes de ejecutar la síntesis. El código usa ADC; no inicia sesión ni cambia tu configuración en la nube. El texto del documento se envía a Google y las solicitudes pueden generar cargos. Empieza con texto de muestra que no sea sensible.
Crea un directorio de trabajo nuevo y vacío, abre allí una terminal Bash y guarda lo siguiente como
Gemfile.
La fuente de gems explícita permite que Bundler descargue las dependencias:
source 'https://rubygems.org'
gem 'google-cloud-text_to_speech', '2.2.1'
gem 'nokogiri', '1.19.4'
Instala las dependencias en este proyecto y conserva el archivo Gemfile.lock resultante junto con tu código:
bundle config set --local path vendor/bundle &&
bundle install --gemfile ./Gemfile
Script básico de narración de documentos
Guarda esta clase como document_narrator.rb. La validación se realiza antes de crear el cliente,
por lo que mostrar la vista previa del texto o rechazar un destino existente no requiere credenciales
de la nube. El texto extraído debe contener algo más que espacios en blanco y respetar el
límite de contenido de 5.000 bytes de Google.
Por ejemplo, 2.501 copias de é superan ese límite aunque haya 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
La extracción de HTML lee el texto del cuerpo en el orden del documento, inserta un espacio entre
los nodos de texto y elimina los elementos script, style, template y noscript.
No renderiza CSS, no respeta todos los atributos de ocultación ni reproduce el orden de lectura
accesible de un navegador. También puede dividir una palabra que abarque elementos en línea.
Revisa la vista previa del resultado y prepara una versión de texto sin formato cuando este orden
no sea adecuado. El analizador de HTML también limita la profundidad del árbol DOM a 400
y los atributos a 400 por elemento. Los PDF y los archivos de procesadores de texto necesitan
un paso de extracción independiente y quedan fuera del alcance de este script.
Funciones avanzadas y manejo de errores
Guarda este punto de entrada como narrate.rb junto a la clase. Los nombres de archivo deben
tener una representación UTF-8 válida, incluso cuando el shell usa la configuración regional POSIX.
El delimitador -- distingue los argumentos de síntesis del modo de vista previa
y permite usar nombres de archivo que comiencen con un guion.
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
Crea document.html con este pequeño ejemplo:
<!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>
Revisa la vista previa antes de realizar una solicitud de pago:
bundle exec ruby narrate.rb --preview document.html
La vista previa debe contener solo esta oración, aparte de los espacios en blanco que la rodeen:
The copper robot carries a yellow umbrella.
Después, sintetiza el texto:
timeout --signal=TERM --kill-after=2s 60s \
bundle exec ruby narrate.rb -- document.html narration.mp3 en-US
Esto concede 60 segundos al comando completo, incluidos el inicio de Ruby, la carga de credenciales
o la renovación de OAuth y la síntesis. GNU timeout concede otros dos segundos antes
de finalizar por la fuerza un proceso que no responde. Un tiempo de espera agotado es un fallo,
aunque el proveedor haya recibido la solicitud; inspecciona el destino antes de volver a intentarlo.
La finalización forzada puede dejar un archivo temporal .narration-.
Comprueba la narración y resuelve los fallos
Si se ejecuta correctamente, el comando imprime Narration saved y crea
narration.mp3. Abre ese archivo exacto en tu reproductor MP3 y escúchalo hasta el final,
comparándolo con la vista previa. Que un MP3 no esté vacío o se pueda reproducir no basta para
confirmar que se haya narrado el texto correcto. La configuración de idioma de la voz no traduce
el documento; usa un código de idioma que coincida con el texto.
El directorio padre del destino debe existir previamente y admitir enlaces duros. Se rechaza cualquier archivo o enlace simbólico existente, y la publicación del archivo también falla si otro proceso crea esa ruta durante la síntesis. Un fallo del proveedor deja intacto un destino existente. Elige otro destino para realizar una segunda grabación; el script no tiene opción de sobrescritura.
Los errores de entrada indican qué comprobación falló. Si el documento de origen supera 1 MiB o el texto extraído supera 5.000 bytes, prepara un documento más pequeño en lugar de truncarlo sin avisar. Si se rechaza el HTML por su anidamiento o sus atributos, prepara una versión de texto sin formato. Ante un fallo genérico, prueba primero la vista previa y después comprueba los permisos del sistema de archivos y las ADC, la facturación, el acceso a la API y la conexión de red del proyecto seleccionado. Los detalles de errores del proveedor y el texto del documento se excluyen de los mensajes de diagnóstico de la síntesis. Una narración más larga necesita un flujo de trabajo independiente de división en fragmentos y ensamblaje; este ejemplo realiza deliberadamente una sola solicitud síncrona.
