はじめに
標準入力やファイルを引数にとって、mp3を出力したい。
ChatGPT API課金勢なら、OpenAIのAPI叩くのが一番簡単。
echo "この文章をスピーチして" | speech.rb -o out.mp3
gistでもいいのだが、検索しにくいのでQiita。
使うもの
- OptionParser - オプション引数の解析
- ruby-openai - OpenAIのAPIを叩くライブラリ
- whirly - 待ち時間にスピナーを表示
スクリプト本体
speech.rb
#! /usr/bin/env ruby
require 'optparse'
require 'openai'
require 'whirly'
options = {
output: 'speech',
model: 'tts-1',
voice: 'alloy',
response_format: 'mp3',
speed: '1.0',
input: nil
}
OptionParser.new do |opts|
opts.banner = "Usage: #{File.basename($0)} [options]"
opts.on('-o', '--output FILE', '出力ファイル名(拡張子なし) [speech]') do |v|
options[:output] = v
end
opts.on('-m', '--model MODEL', '使用するモデル [tts-1] tts-1-hd') do |v|
options[:model] = v
end
opts.on('-i', '--input FILE', '入力テキスト') do |v|
options[:input] = v
end
opts.on('-v', '--voice VOICE', '使用するボイス [alloy] echo fable onyx nova shimmer') do |v|
options[:voice] = v
end
opts.on('-f', '--response-format FORMAT', '出力形式 [mp3] opus aac flac wav pcm') do |v|
options[:response_format] = v
end
opts.on('-s', '--speed SPEED', '読み上げ速度 [1.0] 0.25 - 4.0') do |v|
options[:speed] = v
end
opts.on('-h', '--help', 'このヘルプメッセージを表示') do
puts opts
exit
end
end.parse!
options[:input] ||= ARGF.read
client = OpenAI::Client.new(access_token: ENV['OPENAI_API_KEY'])
Whirly.start spinner: 'dots', remove_after_stop: true
response = client.audio.speech(
parameters: {
model: options[:model],
input: options[:input],
voice: options[:voice],
response_format: options[:response_format],
speed: options[:speed]
}
)
File.binwrite("#{options[:output]}.#{options[:response_format]}", response)
Whirly.stop
感想など
Rubyはこういうスクリプトがさくっとかけて助かる。
Crystalが好きなので、Crystalで実装したかったけど、ライブラリに自分でSpeech APIを追加するのが面倒くさかったのでRubyにした。ruby-openai の実装がどうなっているか確認したけれども、パラメータやレスポンスはクラスで固めておらず、将来OpenAI側のAPIが変更されても動くようになっている。Crystalのような静的言語は、かちっと構造体にする実装を選ばざるをえないことが多く、脆い。その点、動的言語はロバストなところがいい。
こういうの見ていても、静的言語がどんなときも動的言語の上位互換という話はないよなあと思う。(僕はCrystal大好きですが)