PHPとJavaScript(jQuery)で、会話できるシステムのコードを書きました。その簡易版を共有しておきます。
音声会話システムの仕様
仕組み(WhisperとChatGPTの組み合わせ )
- 自分の声をWhisper(text to speech)がテキストにする。
- そのテキストからChatGPTが回答をテキストで生成する
- 生成されたテキストを音声(speech tp text)にする
という流れです。
・音声は「nova」を設定しています。speech tp textの音声には、alloy, echo, fable, onyx, nova, shimmerの6種類があり、novaとshimmerが女性です。
・AIの人格(system)には「友達として雑談してください。テンポよく会話したいので、短い回答でお願いします。」と設定しています。「キャリアカウンセラーとしてキャリアの相談に乗ってください。」「営業ロープレの特訓をしてください」「英会話の練習をしてください」といった形で、ロールを与えられます。
・「会話をする」というシステムの性質上、「短い回答でお願いします。」と指定しています。この指示を加えておかないと、回答時間が長く、会話感が生まれにくいです。
・言語には「ja(日本語)」を指定しています。指定しなくても読み取れますが、たまに言語を間違います。「英会話の練習相手になってください」といったシチュエーションの場合は、外しておいた方が良さそうです。
フロント画面
・下の「録音開始」を押して、話します。(ブラウザで音声入力をします。)
・右側に会話のやり取りが出ます。
・セッションで会話を保持し、左上の「会話履歴をクリア」を押すとやり取りが消えます。
index.php
upload.php
img
L load.gif
L assistant.png
index.php
<?php
session_start();
// セッションをクリアするリクエストをチェック
if (isset($_POST['clear_session'])) {
session_unset();
session_destroy();
}
?>
<!DOCTYPE html>
<html>
<head>
<title>音声録音</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
<script
src="https://code.jquery.com/jquery-3.7.1.js"
integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4="
crossorigin="anonymous"></script>
<style>
.rec_control {
position: fixed;
bottom:0;
width: 70%;
z-index: 1;
}
.main_content {
width: 70%;
}
#response {
position: fixed;
right: 0;
top:0;
padding:4em 2em ;
font-size:12px;
width: 30%;
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
height: 100%;
border-left: 1px solid #eee;
z-index: 1;
background: #fdfdfd;
}
</style>
</head>
<body>
<form method="post">
<button type="submit" name="clear_session" class="btn btn-light border m-2 btn-sm">会話履歴をクリア</button>
</form>
<div class="main_content">
<div class="text-center mb-5">
<img src="img/assistant.png" style="width:260px;border-radius: 100%;">
</div>
<div class="text-center mb-4" id="load_gif" style="display: none;">
<img src="img//load.gif" width="20px;" class="d-inline-block me-1">
<span class="small text-secondary">please wait</span>
</div>
<div id="response_now mb-5 mx-auto fw-bold"></div>
<div class="pt-4 text-center">
「こんにちは」から会話を始めてみよう。
</div>
</div>
<div class="rec_control text-center py-3 border-top bg-white" style="">
<button id="startRecord" class="btn btn-danger text-white">録音開始</button>
<button id="stopRecord" disabled class="btn btn-info text-white" style="display: none;">録音停止</button>
<!-- セッションをクリアするボタン -->
</div>
<div id="response">
<?php if(isset($_SESSION['message_history'])){?>
<?php
$message_history_array = $_SESSION['message_history'];
$message_history_array = array_reverse($message_history_array);
?>
<?php foreach ($message_history_array as $value) { ?>
<?php if($value["role"] =="assistant"){?>
<div class="voice_data voice_data_ai fw-bold pb-1">
友達: <?php echo $value["content"];?>
</div>
<?php } ?>
<?php if($value["role"] =="user"){?>
<div class="voice_data voice_data_user pb-2">
自分: <?php echo $value["content"];?>
</div>
<?php } ?>
<?php } ?>
<?php } ?>
</div>
<script>
let mediaRecorder;
let audioChunks = [];
$("#startRecord").click(function () {
$('#startRecord').css('display', 'none');
$('#stopRecord').css('display', 'inline-block');
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.ondataavailable = e => {
audioChunks.push(e.data);
};
mediaRecorder.onstop = e => {
$('#startRecord').css('display', 'inline-block');
$('#stopRecord').css('display', 'none');
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
const formData = new FormData();
formData.append("voice", audioBlob, "voice.wav");
$.ajax({
url: "upload.php",
type: "POST",
data: formData,
processData: false,
contentType: false,
success: function(response) {
$('#response').prepend(response);
$('#load_gif').css('display', 'none');
}
});
audioChunks = [];
};
mediaRecorder.start();
$("#stopRecord").prop("disabled", false);
});
});
$("#stopRecord").click(function () {
mediaRecorder.stop();
$("#stopRecord").prop("disabled", true);
$('#load_gif').css('display', 'block');
});
</script>
</body>
</html>
upload.php
<?php
session_start();
// 音声(alloy, echo, fable, onyx, nova, shimmerから選択。novaとshimmerが女性。)
$voice_assistant = "nova";
// API KEY
$openai_api_key = 'xxxxxxxxxxxxxxxxxxxx';
// system(AIの役割、人格)
$system_set = "友達として雑談してください。テンポよく会話したいので、短い回答でお願いします。";
// ユーザー履歴の制限(何回分まで保持するか)
$chat_limit = 30;
// chatGPTのモデル
$model ="gpt-4-0125-preview";
// 言語(指定した方が読み取りやすいです。)
$language = "ja";
// 音声データをアップロード
if (!empty($_FILES['voice']['tmp_name'])) {
$upload_dir = 'voice_upload/';
if (!file_exists($upload_dir)) {
mkdir($upload_dir, 0777, true);
}
$upload_file = $upload_dir . basename($_FILES['voice']['name']);
if (move_uploaded_file($_FILES['voice']['tmp_name'], $upload_file)) {
echo '<div class="d-none">ファイルがアップロードされました: ' . $upload_file.'</div>';
// ダウンロードしたファイルを一時保存するパス
$tempFilePath = '/path/to/temp/file.mp3';
// 音声データをテキストに変換(speech to text)
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.openai.com/v1/audio/transcriptions');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
$headers = [
'Authorization: Bearer ' . $openai_api_key,
'Content-Type: multipart/form-data'
];
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$postFields = [
'file' => new CURLFile($upload_file),
'model' => 'whisper-1',
'language' => $language,
];
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
$response = curl_exec($ch);
if (curl_errno($ch)) {
echo 'Error:' . curl_error($ch);
}
curl_close($ch);
if ($response) {
$responseData = json_decode($response, true);
if (isset($responseData['text'])) {
// セッションにメッセージ履歴を保持
if (!isset($_SESSION['message_history'])) {
$_SESSION['message_history'] = [];
}
$responseData2 = null;
// フォームが送信されたかチェック
$userQuestion = $responseData['text'];
if (count($_SESSION['message_history']) < 1) {
$_SESSION['message_history'][] = [
'role' => 'system',
'content' => $system_set,
];
}
// 新しいユーザーメッセージを履歴に追加
$_SESSION['message_history'][] = ['role' => 'user', 'content' => $userQuestion];
// メッセージ履歴の保有数を制限
if (count($_SESSION['message_history']) > $chat_limit) {
array_shift($_SESSION['message_history']);
}
// cURLセッションを初期化
$ch = curl_init('https://api.openai.com/v1/chat/completions');
// リクエストのJSON本体を構成
$data = json_encode([
'model' => $model,
'messages' => $_SESSION['message_history']
]);
// cURLオプションを設定
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $openai_api_key
]);
// リクエストを実行し、レスポンスを取得
$response2 = curl_exec($ch);
curl_close($ch);
// レスポンスをデコード
$responseData2 = json_decode($response2, true);
// レスポンスから得られたAIのメッセージを履歴に追加
if (isset($responseData2['choices'][0]['message'])) {
$_SESSION['message_history'][] = $responseData2['choices'][0]['message'];
}
} else {
echo 'テキスト変換に失敗しました。';
echo '<div class="d-none">';
print_r($response);
echo '</div>';
}
} else {
echo 'APIレスポンスがありません。';
}
} else {
echo "ファイルのアップロードに失敗しました。";
}
} else {
echo "ファイルが見つかりません。";
}
?>
<?php if ($responseData2){ ?>
<!-- AIの回答 -->
<div class="voice_data voice_data_ai fw-bold pb-1">
友達:
<?php echo htmlspecialchars($responseData2['choices'][0]['message']['content']); ?>
</div>
<!-- フロントにも表示 -->
<script>
$('#response_now').append("<?php echo $responseData2["choices"][0]["message"]["content"];?>");
</script>
<?php if ($responseData2["choices"][0]["message"]["content"]) { ?>
<?php
// 音声化(text to speech)
$text = $responseData2["choices"][0]["message"]["content"];
$data = [
'model' => 'tts-1',
'input' => $text,
'voice' => $voice_assistant,
'language' => $language,
];
$ch = curl_init('https://api.openai.com/v1/audio/speech');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $openai_api_key,
'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$response2 = curl_exec($ch);
curl_close($ch);
// 音声データを作成
$file_ver = rand(10000001, 99999999);
file_put_contents('voice_data/speech_'.$file_ver.'.mp3', $response2);
?>
<!-- audio を生成 -->
<audio controls autoplay style="display: none;">
<source src="voice_data/speech_<?php echo $file_ver;?>.mp3" type="audio/mpeg" >
Your browser does not support the audio element.
</audio>
<?php } ?>
<?php } ?>
<?php
// 自分の入力音声の書き起こし
if (isset($responseData['text'])) {
echo '<div class="voice_data pb-3 voice_data_user pb-2">自分: ';
echo $responseData['text']; // 変換されたテキストを出力
echo '</div>';
}
?>
Whisper API の性能
かなり良い感じです。音声を読み取る力、ナチュラルな日本語、ともに優れています。レスポンスのスピードも速いです。
また、「Whisper」という名前もあってか、ヒソヒソ声でも読み取ってきたのはビックリです。
Whisper API の料金
モデル | 説明 | 料金 |
---|---|---|
Whisper | 音声をテキストに | $0.006 / 分 |
TTS(Text-to-Speech) | テキストを音声に | $0.015 / 1000文字 |
コスパはかなり良いと思います。
Whisper API を使う場面
読み上げ、聞き取りの機能はかなり高いですが、若干のタイムラグがあります。また、会話をするには結局AIの回答力が重要になりますので、現時点で実務で使うにはやや限定的な印象です。
けれども、性能はとても素晴らしいので、これから変換速度がさらに速くなり、生成AIの能力があと1,2段階上がれば、コールセンターの代替が可能になってくると思います。数年後には、全言語対応のコールセンターが実現可能になりそうです。
また、言語の学習シーンにおいては、もうかなり実践的な段階で活用できると思います。DMM英会話のようなオンライン英会話サービスは代替できそうです。