レンタルサーバでWebアプリ作り:リアルタイム録音
ロリポップレンタルサーバではできることが限られていたので、ホスティングサービスの DigitalOcean を使ってWebアプリを公開するまでの道のりを記録します。
データ分析を仕事としているのでパソコンやITのことは多少は詳しいですが、インフラまわりは全くの素人です。そんな人が見て参考になる情報をまとめていきたいと思います。
DigitalOceanの紹介リンク [PR]
ここから手続きを進めてもらえると200ドル分の無料チケット(有効期間2ヶ月)がもらえるようですので、ぜひご活用ください。
※ 2023/12/29時点
https://m.do.co/c/a8b31ed34b75
背景
過去の記事で「しゃべってチャットボット」を作りましたが、録音時間が長いとエラーになるという事象が発生したため、リアルタイム録音に変更しました。録音している間、一定期間でデータを送り続けるような仕様に変更します。
ゴール=音声データを送り続ける
この前作ったものは「録音停止ボタンが押されてから一気にデータ送信」でしたが、「録音停止ボタンが押されるまでちょこちょこデータを送る」に変更します。
箱づくり
いつものようにGPT先生に箱を作ってもらいます。以前、socketioのバージョンが使い物にならないものをGPT先生が選んだことがあったので、こちらで指定してあげます。
Flaskで以下のようなプログラムを作りたい。
・ファイルは2つ。app.pyとtemplates/index.html
・通信はsocketioを使う。以下のプログラムを使う。ポート5000で通信する。
・HTMLには「録音開始」と「録音停止」ボタンがある。具体的な機能実装は何もなしでOK。
socketio関連JS: """
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.0/socket.io.js"></script>
"""
https対応
DigitalOceanではhttpsで接続しないといけないので、以下のように修正してもらいます。単にローカル環境でテストするだけなら以下は不要です。
socketioは、以下を使ってください。
// Socket.IOの接続設定
var socketUrl;
var options = { transports: ['websocket'] }; // WebSocketのみを使用
if (location.protocol === 'https:') {
socketUrl = 'https://' + document.domain;
} else {
socketUrl = 'http://' + document.domain + ':5000';
}
var socket = io.connect(socketUrl, options);
録音機能の実装
以下のプロンプトだけでガツガツと録音機能のコードを書いてくれます。
録音機能を実装してください。ポイントは以下の通り。
・種類はaudio/webm
・オーディオデータをArrayBufferに変換してBase64にエンコードする
今回の目玉=データを送り続ける
ここまでは前回のおさらいみたいな話で、今回の目玉は以下の「送り続ける」部分です。
以下のように修正してください。
・録音開始ボタンを押したら録音をスタートさせ、一定期間でPythonにデータを送り続ける(5秒間隔)
・録音停止が押されたら、最後の録音部分を送り、録音を停止する。Pythonに停止信号を送る
・Pythonでは、停止信号がやってきたら録音データを保存する。
ユーザーごとの切り分け
前回発生したトラブル対応部分です。
以下の機能を追加してください。
・アクセスユーザーごとに動くよう、session_idを取り入れてください。
Pythonから停止信号
これで最後。
Python側での保存が終了したら、HTMLに"処理完了"という文字列を送り、HTML側ではその信号を受けて録音開始ボタンを有効にしてください。
トラブルポイント
以下のようなポイントで少々はまりました。
最後のパートが保存されない:7秒話したとして、最初の5秒は録音されるけど6~7秒のパートが録音されていない
録音が5秒以内だとエラーが出る:「最初の5秒のデータが送られてきたときにsession_idを作り、停止ボタンが押されたら最終パートを追加して保存する」という形になっていると、5秒以内の場合にエラーになります。
コード全文
GPT先生が作ってくれたプログラムは以下の通り。 .env を読み込むなど、過去のソースコードも一部取り入れています。
app.py
from flask import Flask, render_template, request
from flask_socketio import SocketIO
import os
import base64
app = Flask(__name__)
app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'your_default_secret_key')
socketio = SocketIO(app)
audio_chunks = {}
@app.route('/')
def index():
return render_template('index.html')
@socketio.on('sending_recdata')
def sending_recdata(data):
print('on recording')
session_id = request.sid
chunk_data = data.get('webmdata')
if session_id not in audio_chunks:
audio_chunks[session_id] = []
audio_chunks[session_id].append(base64.b64decode(chunk_data))
@socketio.on('stop_recording')
def stop_recording(data):
session_id = request.sid
chunk_data = data.get('webmdata')
# session_id に対応するエントリーが存在しない場合は、新しく作成する
if session_id not in audio_chunks:
audio_chunks[session_id] = []
audio_chunks[session_id].append(base64.b64decode(chunk_data)) # 最後のチャンクを追加
complete_audio = b''.join(audio_chunks.get(session_id, []))
filename = f"{session_id}.webm"
with open(filename, 'wb') as file:
file.write(complete_audio)
# 処理後、辞書からエントリーを削除
if session_id in audio_chunks:
del audio_chunks[session_id]
socketio.emit('response_stt', {'res_stt': '処理完了'}, room=session_id)
if __name__ == '__main__':
socketio.run(app, port=5000, debug=True)
templates/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>録音アプリ</title>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.0/socket.io.js"></script>
</head>
<body>
<h1>録音アプリ</h1>
<button id="btn_recording_start">録音開始</button>
<button id="btn_recording_stop" style="display: none;">録音停止</button>
<p id="status"></p>
<script>
let mediaRecorder;
let audioChunks = [];
let recordingInterval;
const RECORDING_INTERVAL_MS = 5000; // 5秒ごとにデータを送信
// Socket.IOの接続設定
var socketUrl;
var options = { transports: ['websocket'] }; // WebSocketのみを使用
if (location.protocol === 'https:') {
socketUrl = 'https://' + document.domain;
} else {
socketUrl = 'http://' + document.domain + ':5000';
}
var socket = io.connect(socketUrl, options);
function startRecording() {
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
const options = { mimeType: 'audio/webm' };
mediaRecorder = new MediaRecorder(stream, options);
mediaRecorder.ondataavailable = async event => {
if (mediaRecorder.state === "recording") {
const base64Data = await getBase64Data(event.data);
socket.emit('sending_recdata', { webmdata: base64Data });
} else if (mediaRecorder.state === "inactive") {
// 録音が停止したときに最後のデータを送信
const base64Data = await getBase64Data(event.data);
socket.emit('stop_recording', { webmdata: base64Data });
}
};
mediaRecorder.start(RECORDING_INTERVAL_MS);
});
}
async function stopRecording() {
mediaRecorder.stop(); // stop すると最後のデータが ondataavailable で取得される
}
async function getBase64Data(blob) {
const arrayBuffer = await blob.arrayBuffer();
return btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
}
document.getElementById("btn_recording_start").addEventListener("click", function() {
startRecording();
document.getElementById("status").textContent = "録音中...";
document.getElementById("btn_recording_stop").style.display = 'block';
});
document.getElementById("btn_recording_stop").addEventListener("click", async function() {
await stopRecording();
document.getElementById("status").textContent = "録音が停止されました。";
document.getElementById("btn_recording_stop").style.display = 'none';
});
</script>
</body>
</html>
最後まで見ていただきありがとうございました!
サポート問い合わせ先
DigitalOceanのサポート問い合わせリンクがなかなか見つからないので、リンクを載せておきます。
https://cloudsupport.digitalocean.com/s/
場所は、トップページの右下にある「Ask a question」に行き、そのページの一番下(欄外っぽいところ)にひっそりと「Support」というリンクがあります(Contact内)。そのページの一番最後に「Contact Support」ボタンがあります。