Whisper API とChatGPT APIをPHPで実装して、音声会話をしてみる

コード

PHPとJavaScript(jQuery)で、会話できるシステムのコードを書きました。その簡易版を共有しておきます。

音声会話システムの仕様

仕組み(WhisperとChatGPTの組み合わせ )

  1. 自分の声をWhisper(text to speech)がテキストにする。
  2. そのテキストからChatGPTが回答をテキストで生成する
  3. 生成されたテキストを音声(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英会話のようなオンライン英会話サービスは代替できそうです。