(個人開発)音声メモからラップを作るLINE Bot をつくったよ


## はじめに

小学校の同級生と一緒に、ポッドキャスト/Spotifyで「これってビジネスチャンスじゃね？ラジオ」という番組を配信しています。日常生活に潜んでいるビジネスチャンスを見つけ出し、新たなビシネスを提案するという内容となっています。一見すると真面目なビジネス番組に聞こえますが、"通学路にいるおじさんの背中に広告をつけよう"とか、"乗り換え案内にダッシュモードをつけよう"とか、しょうもないアイディアをピーチクパーチク喋っているだけのビジネス風バラエティ番組になっています。是非一度聴いてもらえると嬉しいです。

今回はエピソード「【#4】1日の間に録音したボイスメモをその日の終わりにラップにしたくね?」で出たアイディアの "音声メモからラップを作るサービス"を実際に作ってみました。
https://podcasts.apple.com/jp/podcast/%E3%81%93%E3%82%8C%E3%81%A3%E3%81%A6%E3%83%93%E3%82%B8%E3%83%8D%E3%82%B9%E3%83%81%E3%83%A3%E3%83%B3%E3%82%B9%E3%81%98%E3%82%83%E3%81%AD-%E3%83%A9%E3%82%B8%E3%82%AA/id1667836167?i=1000598055774

## 作ったサービス
LINE Bot :ラップ生成

### こんなことができます
- LINEで録音した音声ファイルからラップ風の音声ファイルを生成することができます

### こんな方におすすめ
- 日常的にメモをよくとる人
- メモが埋もれてしまって、メモを活かしきれていない人
- ちょうど、音声メモでラップ作りたいな〜と思っていた人

## 使い方

使い方はとても簡単です。

(1)Lineで https://lin.ee/1NLvYJP を友達追加する
(2)音声録音機能で音声ファイルを送る(ラップっぽく録音する)

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108354/5ecf68d9-bf13-4b4a-bca4-c34865159839.jpeg" width=50%>



(3)メニュー画面の"ラップを生成する"を押す(４つ以上の音声ファイルが保存されている事が前提)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108354/ec6e866a-00f5-eacf-0522-a0b83a63469e.png" width=50%>


(4)URLが送られてくるので音声ファイルをダウンロードする


## 注意点
- 重要なメモは送信しないでください。何か問題があった場合、責任を負いかねます。
- 他のユーザーの音声メモは聴くことはできないように設定されていますが、完全に保証するものではありません。
- この機能はあくまでも遊びや試しに使ってください。
- 6月になったら一度サービスを停止する予定です(お小遣いでやりくりしているのでご了承ください。。状況によっては継続するかも)


## 開発の目的

## ラジオのネタを作るため
現在(14話収録)時点で、お互いが温めてきたビジネスチャンスのアイディアがほぼ枯渇しかけている危機的状況で、番組終了の気配をほんのり感じていました。この状況から脱却するために、新しい方向性の模索として、`今まで出したビジネスチャンスを実際に形にしてみる`という事でした。とりあえず、今回は、ラップを作る謎のサービスを作ってみましたが、これからもネタ作りのためにガンガン作っていく予定です。


## 自分の成長のため
エンジニアを目指した当初から、いずれは個人開発でオリジナルサービスを立ち上げたいと考えていましたが、色々と言い訳をして、行動に移すことができていませんでした。今回、ラジオ番組の企画の一環として、サービスを開発しましたが、自分にとっても良い機会となりました。。ラジオの収録の度に素早く開発をし続けていく事がもしできたら、エンジニアとして、面白い存在になれる気がしていて、それを考えるだけでオラワクワクすっぞ。

## 技術スタック
qiitaなので技術のことも書いておきます。
音声メモからラップ(曲)を生成する事を実現するためのアーキテクチャとして、二つのAPIを作りました。

## (1) Line Message APIから応答するAPI
使用言語: go(本業でいつもGOを使っているため)
インフラ: Google Cloud Platformの cloud function
機能: 
Lineから送られてくるメッセージに応じて特定の処理を実行する
  - "音声ファイル"が送られてきた場合は、
     - GCPのcloud storageに音声ファイルを保存する

  - "テキストメッセージ"が送られてきた場合は、
     - "ラップを生成"のテキストの場合は、"(2)曲生成API"をコールする
     - それ以外の場合は、何も処理を行なわない


## (2) ラップ(曲)を生成するAPI
使用言語: python(音声ファイルを簡単に編集できるpydubライブラリを使いたかった為)
インフラ: GCPのcloud function
機能: 
- GCPのgcloud storageに保存している音声ファイルでラップを生成する機能

処理の流れ: 
  1. APIリクエスト内に含まれているLINEのユーザーIDを取得する
  1. ユーザーIDに紐づく、音声ファイルをgcloud storageをダウンロードする
  1. ダウンロードした音声ファイルをもとにラップを生成する
  1. 生成したラップをgcloud storageにアップロードする
  1. 生成したラップの署名付きURLをラインのメッセージで返却する



## インフラ構成図

![rap-generator-simple-5.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108354/49bc73b9-3230-0ac5-b8e1-ce3b3eda745b.jpeg)


## 今回学んだこと
### マイクロサービス的なアーキテクチャって便利

最初はGO言語だけで全ての機能を実装するつもりでしたが、GOで`音声ファイルを簡単に編集するライブラリ`が見つからなかったので、過去に利用した事があったPythonの`pydubライブラリ`を利用することにしました。。そこで、二つの機能をAPIで連携させるアーキテクチャを採用することで、別の言語であろうと容易にその機能を呼びだす事ができました。(このマイクロサービス的な考え方は今の現場のリーダーから教えてもらいました。ありがとう我がリーダー)

#### Line Message APIで音声ファイルの取得する方法(GO言語)

[line-bot-sdk-go](https://github.com/line/line-bot-sdk-go)パッケージでラインから送信されてきた音声ファイルを簡単に取得する事ができます。ネットで探しても情報を見つける事ができなかったので、コードを残しておきます。
※channelSecretとchannelToken

#### コード
```go
import 	"cloud.google.com/go/storage"
import 	"github.com/line/line-bot-sdk-go/linebot"

func main(){

bot, err := linebot.New("channelSecret","channelToken")
if err != nil{}
//Line Message APIからのリクエストボディをパースする
events, err := bot.ParseRequest(req)
if err != nil {}
for _, event := range events {
    if event.Type == linebot.EventTypeMessage {
        //テキストメッセージタイプがaudioだった場合
        case *linebot.AudioMessage:
		messageID := message.ID
				//lineサーバーから音声ファイルをダウンロードする
				userID := event.Source.UserID
				resp, err := bot.GetMessageContent(messageID).Do()
				if err != nil {
					log.Printf("err:GetMessageContent:%+v", err)
				}
				defer resp.Content.Close()
				//byte型に変換する
				b, err := ioutil.ReadAll(resp.Content)
				if err != nil {
					log.Fatalf("err:ioutil.ReadAll:%+v", err)
				}
				
				//google storageに保存するためのクライアントを作成
				ctx := context.Background()
				client, err := storage.NewClient(ctx)
				if err != nil {
					log.Fatalf("err:storage.NewClient:%+v", err)
				}
				defer client.Close()
				n := time.Now().Format("2006-01-02")
				basePath := userID + "/" + n + "/" + messageID + ".mp4"
				w := client.Bucket(bucketName).Object(basePath).NewWriter(ctx)
				defer w.Close()
				//google storageに保存する
				if _, err := w.Write(b); err != nil {
					log.Fatalf("err:w.Write:%+v", err)
				}
				if err := w.Close(); err != nil {
					log.Fatalf("err:w.Close:%+v", err)
				}
				//処理終了後にlineで返信する
				if _, err := bot.ReplyMessage(event.ReplyToken, linebot.NewTextMessage("メモを保存したぜ。サンキューな")).Do(); err != nil {
					log.Fatalf("err:bot.ReplyMessage:%+v", err)
				}
}}}
```

### google storageの署名付きURLってええやん

最終的に生成したラップをLINEで送り返す際に、毎回音声ファイルをダウンロードしてLINEのメッセージに添付する方法を最初は検討していました。しかし、毎回音声ファイルをダウンロードすると、コストがかかることが気掛かりになってましたが、Google Cloud Storageの署名付きURLを利用することで、簡単に音声ファイルにアクセスできるようになり、コストを抑えることができることに気づきました。

#### 署名付きURLとは
Google Cloud Storage に保存されたオブジェクトに対して、一定期間だけアクセスするためのURLです。通常のURLとは異なり、署名付きURLは特定のアクセス権を持ち、期限が切れると自動的に無効になります。



音声ファイルを返すのではなく、URL(文字列)を返せばいいだけで済むのでとても便利ですね。

#### コード
```golang
//google storageから生成したラップを取得するための処理
timeFormated := time.Now().Format("2006-01-02")
//google storageのラップが保存されているパス
basePath := userID + "/output" + "/" + timeFormated + "/rap_business_chancejane.mp4"
if err != nil {
		log.Fatalf("err:storage.NewClient:%+v", err)
}
//有効期限を設定(有効期限は10分)
expiredTime := time.Now().Add(time.Minute * 10).String()
expires, _ := time.Parse(time.RFC3339, expiredTime)
opts := &storage.SignedURLOptions{
		Method:  http.MethodGet,
		Expires: expires,
}
ctx := context.Background()
client, err := storage.NewClient(ctx)
if err != nil {
	log.Printf("err:storage.NewClient(ctx):%+v", err)
}
url, err := client.Bucket(bucketName).SignedURL(basePath, opts)
if err != nil {
		log.Fatalf("err:storage.SignedURL:%+v", err)
}
//署名付きURLをラインのメッセージで返す
if _, err = bot.ReplyMessage(event.ReplyToken, linebot.NewTextMessage(url)).Do(); err != nil {
		log.Fatalf("err:bot.ReplyMessage:%+v", err)
}
```

### 音声ファイルの編集

GO言語では、簡単に音声ファイルを編集できるライブラリが見つからなかったので
pythonのpydubを使うことにしました。

#### pydubとは
Pydubとは、オーディオファイルをPythonを使って読み込み、音声処理を実装できる。
https://github.com/jiaaro/pydub


## 今回できなかった事
ラジオを聴いてもらうとわかりますが、ラジオ内で意気揚々と語っていた当初の内容とは掛け離れたものになってしまいました。

### ラジオで言っていた仕様
- 1日の終わりに曲にしてくれる。
- ラップのタイプが選べる（希望はMOROHA、エミネムやクリーピーナッツも選択可能)。
- 友達とコラボできる機能あり(「今日のラップはフィーチャリング ●●●●さんです」みたいな感じ。)

### 言い訳

> 1日の終わりに曲にしてくれる機能。

一日の終わりにメッセージ配信機能でラップを一方的に送りつけてやろうかと思ってましたが、今回はLINEのフリープラン(メッセージ配信を200通までしか行えないプラン)だったので、こちら側からの配信はやめました。コストをより安く済ますために仕様をシンプルにしました。

> ラップのタイプが選べる（希望はMOROHA、エミネムやクリーピーナッツも選択可能)。

元々は音声メモをサーバーサイドでラップっぽく加工しようと考えてましたが、全く上手くいきませんでした。音声編集の複雑さと難易度の高さを完全に舐めてました。声質を調整して、スピードを上げれば少しはラップぽい仕上がりになると思ってましたが、、残念ながらラップっぽくならず、ただ音声ファイルをつなぎ合わせただけのような仕上がりになってしまいました。
AIによる音声特徴変換を利用することで自分がやりたい事を実現できそうだったのですが、学習コストが高い、そもそも、AIを動かすための環境の準びができないという理由で、今回は挫折してしまいました。
しかし、その代わりに、利用者にラップ風に録音してもらうという逆転の発想で対応しようとしています(笑)
こんなしょぼいサービスを生み出してしまってすみません.何かいい方法があればぜひ教えてください。

> 友達とコラボできる機能あり

これはラインのグループ機能を使えば、実現できそうです。もし要望の声が多数あれば実装します。

## 最後に


今後もラジオのネタのために個人開発を続けていきたいと考えています。
今回しょぼしょぼサービスになってしまったのですが、一つの形にできたことはに些さかの自信に繋がりました。まだまだ未熟なエンジニアですが、リスナーの方々の支援も受けながら、成長していきたいと思っています。

この度は最後まで読んで頂きありがとうございました。引き続き、「これってビジネスチャンスじゃね？ラジオ」をよろしくお願いします。