1
Help us understand the problem. What are the problem?

posted at

updated at

Organization

Twilioで漫才をする?

はじめに

2022年6月末でCodefontのライトプランでのAPI利用は廃止されました。
Coefontが面白くいろいろ遊んできましたので、最後にTwilioを絡めて成仏させました。

記載しているコードは、集大成ということもあり、過去にの記事にもあるAzure Functionなどを使用したものになっています。

過去記事

Twilioで漫才をやってみる。

大好きな赤い芸人さんのCoefontを活用させていただきます。
芸人さんには、芸をしてもらいたいということで、Twilioを使い、一人でしゃべくり漫才ができる作品にしました。

また、今回行った漫才は、こちらも個人的に大好きなミルクボーイさんのフォーマットをお借りしてます。

漫才ネタ(一部)
ボケ   「いきなりですけどね うちのオカンがね 好きな電話APIがあるらしいんやけど」
ツッコミ 「あっ、そーなんや」
ボケ   「その名前をちょっと忘れたらしくてね」
ツッコミ 「電話APIの名前忘れてもうて、どうなってんねそれ」

ボケを自分が、ツッコミを赤い芸人が担当。
漫才は、ボケとツッコミの掛け合いがありますが、この掛け合いをTwilioだけでできるようにしました。

実現方法

処理の流れ位は以下の通り、

  1. Twilioを使って声を録音する
  2. 録音情報をテキスト化し、Coefontで赤い芸人の音声に変換する
  3. 録音した音声を再生する
  4. 再生した音声と漫才を行う

変換には時間がかかる為、リアルタイムではなく、再度電話時に録音した音声を再生させます。

Twilioを使って声を録音する

間を取る

掛け合いを行うには、ボケのセリフの時間分、無音が必要となります。
この間を作成するため、ツッコミの台詞毎に音声を作成し、セリフの前に無音時間を付与させ、連続で再生することで実現させます。

無音を録音

Twilioの録音< Recoad >において、無音の音声は、削除される設定になっています。
まずは、設定を変更して、無音情報を有効にします。

有効にするには、tirmパラメータdo-not-trimを設定し、無音を有効にします。
さらにtimeoutパラメータの時間を変更し、無音時間の録音を長くします。
このパラメータを変更しないと5秒以上の無音が続くと録音が終了します。

これらをコードにすると以下のようになります。

録音時の処理
var response = new VoiceResponse();

string msg = "あなたの声を、あかい芸人の声に変換します。";
msg += "発信音の後に、メッセージを、30秒以内でお話しください。";
                    
response.Say(msg, language: "ja-jp", voice: voice_type);
response.Record(playBeep:true, maxLength:30, finishOnKey: "#", trim:"do-not-trim", action:api_url + "&mode=record", timeout:6);
            
return new ContentResult{Content = response.ToString(), ContentType = "application/xml"};

録音情報をテキスト化し、Coefontで赤い芸人の音声に変換する

録音が完了したら、音声をテキスト化し、テキストから赤い芸人の音声を作成します。

音声をテキストに変換

まずは、録音した音声をテキスト化します。
テキスト化は、AzureのSpeech to Text REST API for short audioを使用しました。
Speech SDKの利用が推奨されているようですが、短い時間の録音で手軽だったのでこちらを使用

このAPIを使用すると以下のような結果が取得できます。
結果には、テキスト(DisplayText)以外にも、喋り始めまでの時間(Offset)も取得可能です。
このOffset時間を漫才の間として使用します。

注意
Speech to Text REST API for short audio版のAPIの場合、5秒以上の無音はエラー(InitialSilenceTimeout)になるようです。
5秒以上を取得したい場合は、Speech SDK版を使用すればこの時間を設定できそうです。

応答パラメータ例)

応答情報
{
  "RecognitionStatus": "Success",
  "DisplayText": "Remind me to buy 5 pencils.",
  "Offset": "1236645672289",
  "Duration": "1236645672289"
}

Record時の保存形式の初期設定はWavですので、Wavを想定した動作になっています。

変換時の処理
// 録音のバイナリデータを取得する
var response = await httpClient.GetAsync(req.Form["RecordingUrl"]);
byte[] record_byte = response.Content.ReadAsByteArrayAsync();

// 音声をテキスト化する (Speech to Text REST API)
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, speech_to_text_url);
request.Headers.Add("Ocp-Apim-Subscription-Key", subscriptionKey);

// 録音バイナリをContentsに設定
var httpContent = new ByteArrayContent(record_byte);

// ContentTypeをWavに設定 
httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(@"audio/wav");
request.Content = httpContent;

// APIの呼び出し
var response = await httpClient.SendAsync(request);
if(response.IsSuccessStatusCode){
  return JsonConvert.DeserializeObject<SpeechToTextResult>(resString);
}

Coefontに変換

テキスト化された情報を赤い芸人の声に変換します。
※詳しくは 「CoeFontをC#で使ってみる」をご覧ください。
Coefone API仕様:https://docs.coefont.cloud/

変換処理
string unixTime = new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds().ToString();

// Coefontの指定や、変換の設定
var body = new Dictionary<string, object>()
{
  { "coefont", coe_takahashi_font},
  { "text"   , text},
  { "format" , "wav"}
};

//認証キーの作成          
var body_json = JsonConvert.SerializeObject(body);
var hmac_key = Encoding.UTF8.GetBytes(coe_secret);
var hmac_body = Encoding.UTF8.GetBytes(unixTime + body_json);
                
string signature = "";
using (HMACSHA256 hmac = new HMACSHA256(hmac_key)){
  var hash = hmac.ComputeHash(hmac_body, 0, hmac_body.Length);
  signature = BitConverter.ToString(hash);            
}
signature = signature.Replace("-", "").ToLower();

HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, coe_url);
request.Headers.Add("X-Coefont-Date", unixTime);
request.Headers.Add("X-Coefont-Content", signature);
request.Headers.Add("Authorization", coe_accessKey);

// 設定の指定
var httpContent = new StringContent(body_json, Encoding.UTF8, "application/json");  
request.Content = httpContent;
             
var response = await httpClient.SendAsync(request);
if(response.IsSuccessStatusCode){
  //保存処理
}

変換したデータは、ストレージアカウントに保存します。

保存処理
var blobServiceClient = new BlobServiceClient(connectionString);
var blobContainerClient = blobServiceClient.GetBlobContainerClient(blobContainerName );
await blobContainerClient.CreateIfNotExistsAsync(PublicAccessType.None);
var blobClient = blobContainerClient.GetBlobClient(tel + unixTime);

// データをアップロードする
await blobClient.UploadAsync(await response.Content.ReadAsStreamAsync());

// ContentTypeをWavに設定
var properties = blobClient.GetPropertiesAsync();
BlobHttpHeaders headers = new BlobHttpHeaders
{
  ContentType = "audio/wav"
};
await blobClient.SetHttpHeadersAsync(headers);  

変換情報を保存

変換後のファイル名や、再生開始までの間の情報をCosmosDBに登録します。
※変換前のテキストや、録音元のURLなんかもテスト的に保存してます。

DB保存
Uri collectionUri = UriFactory.CreateDocumentCollectionUri(<<databaseId>>, <<collectionId>>);

string t_id = System.Guid.NewGuid().ToString();  
 
// ※音声認識時のOffsetは、100ナノ秒単位なので秒に変換
await documentsOut.AddAsync(new
{
  id = t_id,
  tel = call_tel,
  text = text_result.DisplayText,
  record = wav_url,
  audio = audio_id,
  wait = (int)(text_result.Offset / 10000000.0f)
   }
);

録音した音声を再生する

CosmosDBより情報取得

発信元の電話番号をキーとして、対象となる情報をCosmosDBより取得します。

情報検索
Uri uriFactory = UriFactory.CreateDocumentCollectionUri(<<databaseId>>, <<collectionId>>);
var list = client.CreateDocumentQuery<TakahashiInfo>(uriFactory)
                .Where(p => p.Tel.Equals(req.Form["From"]))
                .ToList();

音声を再生する

取得した情報を元に、間(Wait)が設定されている場合は、音声毎の間に、待ち時間 < Pause >を指定し、
音声の再生 < play >を登録します

音声の再生
var response = new VoiceResponse();

for(int i=0; i<list.Count; i++){
  if(list[i].Wait > 0) {
    // 間を登録
    response.Pause(length: list[i].Wait);
  }
  // 再生する音声ファイル名を登録
  response.Play(blob_url +  list[i].Audio);
}
response.Pause(length: 2);

return new ContentResult{Content = response.ToString(), ContentType = "application/xml"};   

再生されたら、あとは漫才をするだけです。

再生した音声と漫才を行う

こちらが実際のデモ動画(漫才のデモは2分40秒目から)
https://youtu.be/uxYsv9UJDic

補足

システム構成図

全体のシステム構成図は以下の通りです。
image.png

操作フロー

削除とか、1個の音声データ再生とかにも対応してます!
image.png

まとめ

結果としてそれっぽく出来上が多ったのではないかなと思います。
想定しないような使い方をすることで、普段では使わないようなパラメータや設定に出合えたのは良い勉強になりました。

無音に関しても、質問に対して悩んでいるから、応答までが長い・・など意外と業務に転用できたりするかもしれないなと思いました。

関連項目

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
1
Help us understand the problem. What are the problem?