はじめに
2022年6月末でCodefontのライトプランでのAPI利用は廃止されました。
Coefontが面白くいろいろ遊んできましたので、最後にTwilioを絡めて成仏させました。
記載しているコードは、集大成ということもあり、過去にの記事にもあるAzure Functionなどを使用したものになっています。
過去記事
Twilioで漫才をやってみる。
大好きな赤い芸人さんのCoefontを活用させていただきます。
芸人さんには、芸をしてもらいたいということで、Twilioを使い、一人でしゃべくり漫才ができる作品にしました。
また、今回行った漫才は、こちらも個人的に大好きなミルクボーイさんのフォーマットをお借りしてます。
ボケ 「いきなりですけどね うちのオカンがね 好きな電話APIがあるらしいんやけど」
ツッコミ 「あっ、そーなんや」
ボケ 「その名前をちょっと忘れたらしくてね」
ツッコミ 「電話APIの名前忘れてもうて、どうなってんねそれ」
ボケを自分が、ツッコミを赤い芸人が担当。
漫才は、ボケとツッコミの掛け合いがありますが、この掛け合いをTwilioだけでできるようにしました。
実現方法
処理の流れ位は以下の通り、
- Twilioを使って声を録音する
- 録音情報をテキスト化し、Coefontで赤い芸人の音声に変換する
- 録音した音声を再生する
- 再生した音声と漫才を行う
変換には時間がかかる為、リアルタイムではなく、再度電話時に録音した音声を再生させます。
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なんかもテスト的に保存してます。
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
補足
システム構成図
操作フロー
まとめ
結果としてそれっぽく出来上が多ったのではないかなと思います。
想定しないような使い方をすることで、普段では使わないようなパラメータや設定に出合えたのは良い勉強になりました。
無音に関しても、質問に対して悩んでいるから、応答までが長い・・など意外と業務に転用できたりするかもしれないなと思いました。

