Synthétiser la parole à partir de documents avec Ruby
Pour un document court, vous pouvez extraire son texte en Ruby et l’envoyer à Google Cloud Text-to-Speech en une seule requête. L’exemple ci-dessous prévisualise le texte sélectionné et sauvegarde une narration MP3 dans un nouveau fichier. Il accepte le texte brut et le HTML en UTF-8, avec une limite de 1 MiB pour le document source et de 5 000 octets pour le texte extrait.
Configurer Google Cloud Text-to-Speech
Utilisez GNU/Linux avec Ruby et Bundler installés. Ce guide a été testé avec Ruby 4.0.7,
Bundler 4.0.23 et timeout de GNU coreutils ; un lecteur MP3 local est nécessaire pour vérifier le résultat.
Utilisez une version de Ruby encore maintenue, disponible sur la page de téléchargement de Ruby.
La commande de limitation du temps d’exécution ci-dessous dépend de GNU coreutils et n’est donc pas
directement portable sur macOS ou Windows.
Vous avez également besoin d’un projet Google Cloud dont la facturation et l’API Text-to-Speech sont activées, ainsi que d’informations d’authentification autorisées à utiliser cette API. Suivez les instructions de Google pour la configuration du projet et celles du guide Application Default Credentials avant de lancer la synthèse. Le code utilise ADC ; il ne vous connecte pas et ne modifie pas votre configuration cloud. Le texte du document est envoyé à Google et les requêtes peuvent entraîner des frais. Commencez par un exemple de texte non sensible.
Créez un nouveau répertoire de travail vide, ouvrez-y un terminal Bash et enregistrez le contenu
suivant dans Gemfile.
La source des gems explicitement indiquée permet à Bundler de télécharger les dépendances :
source 'https://rubygems.org'
gem 'google-cloud-text_to_speech', '2.2.1'
gem 'nokogiri', '1.19.4'
Installez les dépendances dans ce projet et conservez le fichier Gemfile.lock obtenu avec votre code :
bundle config set --local path vendor/bundle &&
bundle install --gemfile ./Gemfile
Script de base pour la narration de documents
Enregistrez cette classe dans document_narrator.rb. La validation a lieu avant la création du client ;
la prévisualisation du texte ou le refus d’une destination existante ne nécessitent donc pas
d’informations d’authentification cloud. Le texte extrait doit contenir autre chose que des
caractères d’espacement et respecter la
limite de contenu de 5 000 octets imposée par Google.
Par exemple, 2 501 copies de é dépassent cette limite, même si cela représente moins de 5 000 caractères.
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
L’extraction HTML lit le texte du corps dans l’ordre du document, insère un espace entre les nœuds
textuels et supprime les éléments script, style, template et noscript.
Elle n’effectue pas le rendu CSS, ne prend pas en compte tous les attributs de masquage et ne
reproduit pas l’ordre de lecture accessible d’un navigateur. Elle peut aussi scinder un mot réparti
sur plusieurs éléments en ligne. Prévisualisez le résultat et préparez une version en texte brut
si cet ordre ne convient pas.
L’analyseur HTML limite également la profondeur de l’arbre DOM à 400
et le nombre d’attributs à 400 par élément. Les PDF et les fichiers de traitement de texte
nécessitent une étape d’extraction distincte et sortent du périmètre de ce script.
Fonctionnalités avancées et gestion des erreurs
Enregistrez ce point d’entrée dans narrate.rb, à côté de la classe. Les noms de fichiers doivent
être valides en UTF-8, même lorsque le shell utilise la locale POSIX. Le délimiteur
-- distingue les arguments de synthèse du mode de prévisualisation et permet d’utiliser
des noms de fichiers commençant par un trait d’union.
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
Créez document.html avec ce petit exemple :
<!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>
Prévisualisez-le avant d’effectuer une requête payante :
bundle exec ruby narrate.rb --preview document.html
La prévisualisation devrait contenir uniquement cette phrase, à l’exception d’éventuels caractères d’espacement autour de celle-ci :
The copper robot carries a yellow umbrella.
Lancez ensuite la synthèse :
timeout --signal=TERM --kill-after=2s 60s \
bundle exec ruby narrate.rb -- document.html narration.mp3 en-US
La commande entière dispose ainsi de 60 secondes, ce qui comprend le démarrage de Ruby, le chargement
des informations d’authentification ou le renouvellement OAuth, ainsi que la synthèse. La commande
GNU timeout accorde deux secondes supplémentaires avant de forcer l’arrêt d’un processus
qui ne répond pas. Un dépassement du délai constitue un échec, même si le fournisseur a reçu la
requête ; inspectez la destination avant de réessayer. Un arrêt forcé peut laisser un fichier
temporaire .narration-.
Vérifier la narration et gérer les échecs
En cas de réussite, la commande affiche Narration saved et crée
narration.mp3. Ouvrez précisément ce fichier dans votre lecteur MP3 et écoutez-le jusqu’à la fin
en le comparant à la prévisualisation. Un MP3 non vide ou lisible ne suffit pas à établir que le bon
texte a été prononcé. Le réglage de langue de la voix ne traduit pas le document ; utilisez un code
de langue correspondant au texte.
Le répertoire parent de la destination doit déjà exister et prendre en charge les liens physiques. Un fichier ou un lien symbolique existant est refusé, et la publication échoue également si un autre processus crée ce chemin pendant la synthèse. Un échec du fournisseur laisse toute destination existante intacte. Choisissez une autre destination pour effectuer un deuxième enregistrement ; le script ne propose aucune option d’écrasement.
Les erreurs d’entrée indiquent la vérification qui a échoué. Pour un document source de plus de 1 MiB ou un texte extrait de plus de 5 000 octets, préparez un document plus petit plutôt que de le tronquer silencieusement. En cas de refus lié à l’imbrication HTML ou aux attributs, préparez une version en texte brut. En cas d’échec générique, essayez d’abord la prévisualisation, puis vérifiez les permissions du système de fichiers ainsi que les ADC, la facturation, l’accès à l’API et la connexion réseau du projet sélectionné. Les détails des erreurs du fournisseur et le texte du document sont exclus des diagnostics de synthèse. Une narration plus longue nécessite un processus distinct de découpage et d’assemblage ; cet exemple effectue délibérément une seule requête synchrone.
