class NcodeSyosetu::Builder::Polly

Constants

POLLY_TEXT_LENGTH_LIMIT

Attributes

client[R]
logger[R]
sample_rate[R]
service[R]

Public Class Methods

new(options={}) click to toggle source
# File lib/ncode_syosetu/builder/polly.rb, line 15
def initialize(options={})
  options[:region] ||= "us-west-2"
  @logger = options.delete(:logger)
  @sample_rate = options.delete(:sample_rate) || "16000"
  @max_threads = options.delete(:max_threads) || 10
  @client = Aws::Polly::Client.new(options)
  @htmlentities = HTMLEntities.new
  @service = Expeditor::Service.new(
    executor: Concurrent::ThreadPoolExecutor.new(
      min_threads: 0,
      max_threads: @max_threads,
    )
  )
end

Public Instance Methods

create_ssml(body_ssml) click to toggle source
# File lib/ncode_syosetu/builder/polly.rb, line 78
      def create_ssml(body_ssml)
        <<-XML
<?xml version="1.0"?>
<speak version="1.1" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="ja">
#{body_ssml}
</speak>
        XML
      end
split_ssml(body_ssml) click to toggle source
# File lib/ncode_syosetu/builder/polly.rb, line 87
def split_ssml(body_ssml)
  ssml = tweak_ssml(body_ssml)
  doc = Nokogiri::XML.parse("<root>#{ssml}</root>")
  elements = doc.root.children

  buffer = StringIO.new
  text_count = 0
  results = []
  while elements.size > 0 do
    element = elements.shift

    case element
      when Nokogiri::XML::Text
        text = @htmlentities.encode(element.text)
      when String
        text = @htmlentities.encode(element)
      else
        buffer.print(element.to_s)
        next
    end

    if text.size > POLLY_TEXT_LENGTH_LIMIT
      elements = text.chars.each_slice(POLLY_TEXT_LENGTH_LIMIT).map(&:join) + elements
      next
    end

    if (text_count + text.size) > POLLY_TEXT_LENGTH_LIMIT
      results << buffer.string
      buffer = StringIO.new
      text_count = 0
    end
    buffer.print(text)
    text_count += text.size
  end
  results << buffer.string if buffer.size > 0

  results
end
tweak_ssml(body_ssml) click to toggle source
# File lib/ncode_syosetu/builder/polly.rb, line 126
def tweak_ssml(body_ssml)
  body_ssml.
      gsub("<p>", "").
      gsub("</p>", '<break strength="strong"/>').
      gsub(/([」】)』])/, '\1<break strength="strong"/>')
end
write_episode(episode, path) click to toggle source
# File lib/ncode_syosetu/builder/polly.rb, line 30
def write_episode(episode, path)
  tmp_files = []
  ssmls = split_ssml(episode.body_ssml.gsub("\n", "")).map{|body_ssml| create_ssml(body_ssml) }

  commands = []

  dirname = File.dirname(path)
  basename = File.basename(path, ".mp3")
  tmpdir = File.join(dirname, basename)
  FileUtils.mkdir_p(tmpdir)

  ssmls.each_with_index do |ssml, i|
    tmp_ssml_path = File.join(tmpdir, "#{basename}-#{i}.ssml")
    File.write(tmp_ssml_path, ssml)
    tmp_path = File.join(tmpdir, "#{basename}-#{i}.mp3")
    command = Expeditor::Command.new(service: service) do
      logger.info("#{tmp_path}...") if logger
      begin
        client.synthesize_speech(
          response_target: tmp_path,
          output_format: "mp3",
          sample_rate: sample_rate,
          text: ssml,
          text_type: "ssml",
          voice_id: "Mizuki",
        )
      rescue => e
        logger.error("#{e.message}\n#{ssml}")
        logger.error("#{e.message}: #{tmp_ssml_path}\n#{ssml}")
        raise e
      end
    end
    command.start
    commands << command
    tmp_files << tmp_path
  end
  commands.each{|command| command.get }
  File.open(path, "wb") do |file|
    tmp_files.each do |tmp_path|
      File.open(tmp_path, "rb") do |tmp_file|
        IO.copy_stream(tmp_file, file)
      end
    end
    file.flush
  end
  logger.info("Generated: #{path}") if logger
end