Synthesize speech in documents using Ruby
Converting text documents into speech can enhance accessibility and offer users new ways to engage with your content. In this DevTip, we explore how to synthesize speech in documents using Ruby and the Google Cloud Text-to-Speech library, complete with practical examples and best practices.
Setting up Google Cloud Text-to-Speech
Google Cloud Text-to-Speech provides high-quality voices, extensive language support, and seamless integration. Use Ruby 3.2 or newer, enable the Text-to-Speech API and billing in your Google Cloud project, then add these dependencies to your Gemfile:
gem 'google-cloud-text_to_speech', '2.2.0'
gem 'nokogiri', '1.19.4'
Install the bundle and commit its lockfile so the tested dependency graph is reproducible:
bundle install
Use Application Default Credentials. For local development, the Google Cloud CLI can create development credentials:
gcloud auth application-default login
On hosted workloads, prefer an attached service identity over a downloaded long-lived key. Never commit credentials or include document text in diagnostic logs. Actual synthesis requests use the configured account and can incur charges.
Basic document narration script
Save this class as document_narrator.rb. It supports UTF-8 plain text and HTML documents, limits
source-file size, strips non-content HTML elements, and checks the extracted text against the
standard synthesis API's 5,000-byte content limit. The limit is bytes, not characters.
require "google/cloud/text_to_speech"
require "nokogiri"
require "tempfile"
class DocumentNarrator
def initialize(client: Google::Cloud::TextToSpeech.text_to_speech)
@client = client
end
def narrate_text(text, output_path, language_code: "en-US")
unless text.is_a?(String) && text.encoding == Encoding::UTF_8 && text.valid_encoding?
raise ArgumentError, "Text must be valid UTF-8"
end
raise ArgumentError, "Text cannot be empty" if text.strip.empty?
raise ArgumentError, "Text exceeds 5,000 bytes" if text.bytesize > 5000
if File.exist?(output_path) || File.symlink?(output_path)
raise ArgumentError, "Output must not exist"
end
input = { text: text }
voice = { language_code: language_code, ssml_gender: :FEMALE }
audio_config = { audio_encoding: :MP3, speaking_rate: 1.0, pitch: 0.0 }
# Create the candidate on the destination filesystem before making a paid request.
Tempfile.create([".narration-", ".mp3"], File.dirname(output_path)) do |file|
file.binmode
response = @client.synthesize_speech(
input: input, voice: voice, audio_config: audio_config
)
raise IOError, "Synthesis returned empty audio" if response.audio_content.empty?
file.write(response.audio_content)
file.flush
# Publication must not replace a file created while synthesis was running.
File.link(file.path, output_path)
end
end
def narrate_file(input_path, output_path, language_code: "en-US")
text = extract_text(input_path)
narrate_text(text, output_path, language_code: language_code)
end
private
def extract_text(file_path)
raise ArgumentError, "Input must be a regular file" unless File.file?(file_path)
source = File.binread(file_path, 1024 * 1024 + 1)
raise ArgumentError, "Source exceeds 1 MiB" if source.bytesize > 1024 * 1024
source.force_encoding(Encoding::UTF_8)
raise ArgumentError, "Source must be valid UTF-8" unless source.valid_encoding?
case File.extname(file_path).downcase
when ".txt"
source
when ".html", ".htm"
doc = Nokogiri::HTML5(source)
doc.css("script, style, template, noscript").remove
doc.at_css("body").xpath(".//text()").map(&:text).join(" ").strip
else
raise ArgumentError, "Unsupported file format"
end
end
end
Advanced features and error handling
Keep one implementation of DocumentNarrator and add a small CLI around it. Save the following as
narrate.rb in the same directory. It exits unsuccessfully on invalid input, provider failure, or
an output collision, without printing credential-bearing provider errors or document text.
require_relative "document_narrator"
unless (2..3).cover?(ARGV.length)
abort "Usage: bundle exec ruby narrate.rb <input.txt|input.html> <new-output.mp3> [language-code]"
end
begin
DocumentNarrator.new.narrate_file(ARGV[0], ARGV[1], language_code: ARGV[2] || "en-US")
puts "Narration saved"
rescue StandardError
warn "Narration failed. Check the input, destination, credentials, and provider configuration."
exit 1
end
Run bundle exec ruby narrate.rb document.html narration.mp3 en-US. Select a language code matching
your document and a voice configuration supported by the provider. The voice setting does not
translate the input. HTML extraction here is deliberately simple; it does not reproduce browser
layout, CSS visibility, or accessible reading order. Review extracted content before narration.
Best practices for speech synthesis
Consider these best practices when implementing text-to-speech functionality:
-
Text preprocessing:
- Split long text at sentence boundaries and check each request's UTF-8 byte size.
- Follow the current content limits.
- Remove unnecessary whitespace and special characters.
- Expand abbreviations and numbers for improved pronunciation.
-
Voice configuration:
- Select the appropriate language and voice for your content.
- Adjust speaking rate and pitch for natural-sounding audio.
- Leverage SSML markup for fine-grained control, including pauses and emphasis.
-
Performance optimization:
- Implement caching to reduce redundant API calls.
- Use batch processing for large documents to enhance efficiency.
- Use the provider's documented long-audio workflow for material that exceeds synchronous limits.
-
Security considerations:
- Securely store and manage your API credentials.
- Implement rate limiting and input validation to prevent abuse.
- Monitor API usage and set alerts to control costs.
Alternative solutions
While Google Cloud Text-to-Speech is a robust solution, you might consider other options:
- Amazon Polly's Ruby SDK offers a dedicated client. Its voice, engine, and content-limit rules differ from Google's.
- Azure's Text-to-Speech REST API can be called from Ruby. Do not assume an SDK for another language has a Ruby equivalent; follow the REST authentication, SSML, output-format, timeout, and HTTP-error contracts.
- Self-hosted speech models can keep document processing in your own infrastructure. Verify model licensing and maintenance, allocate worker resources, and use the chosen server's documented HTTP API rather than assuming all TTS servers accept the same JSON payload.
Conclusion
Modern text-to-speech solutions empower you to create engaging, accessible audio content from documents using Ruby. Google Cloud Text-to-Speech, along with other alternatives, offers high-quality voices and multi-language support. For managed speech synthesis, see Transloadit's Text to Speech Robot.
