Class: NcodeSyosetu::Builder::Polly

Inherits:
Object
  • Object
show all
Defined in:
lib/ncode_syosetu/builder/polly.rb

Constant Summary collapse

POLLY_TEXT_LENGTH_LIMIT =
1000

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Polly

Returns a new instance of Polly.



15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 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

Instance Attribute Details

#clientObject (readonly)

Returns the value of attribute client.



13
14
15
# File 'lib/ncode_syosetu/builder/polly.rb', line 13

def client
  @client
end

#loggerObject (readonly)

Returns the value of attribute logger.



13
14
15
# File 'lib/ncode_syosetu/builder/polly.rb', line 13

def logger
  @logger
end

#sample_rateObject (readonly)

Returns the value of attribute sample_rate.



13
14
15
# File 'lib/ncode_syosetu/builder/polly.rb', line 13

def sample_rate
  @sample_rate
end

#serviceObject (readonly)

Returns the value of attribute service.



13
14
15
# File 'lib/ncode_syosetu/builder/polly.rb', line 13

def service
  @service
end

Instance Method Details

#create_ssml(body_ssml) ⇒ Object



78
79
80
81
82
83
84
85
# 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) ⇒ Object



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# 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) ⇒ Object



126
127
128
129
130
131
# 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) ⇒ Object



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# 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