sayahaya / voice_component

1 stars 0 forks source link

ブラウザ録音機能を実装 #20

Closed sayahaya closed 3 years ago

sayahaya commented 3 years ago
sayahaya commented 3 years ago

JSを使ったブラウザ録音ボタンの作成をしております。実装でどのようにすればいいか悩んでおり、アドバイスをいただきたくお願い致します。

現状録音ボタンは下記のように出来ているのですが、

Image from Gyazo

  1. ”さっそく声をチェック”ボタンを押す(録音開始)
  2. ”録音を終了”ボタンを押す(録音を終了し、声のデータを生成、話者識別APIに生成した声のデータを投げて識別結果を取得)
  3. ”送信”ボタンを押す(識別結果をサーバー側へ送り、画面遷移)

これを

  1. ”さっそく声をチェック”ボタンを押す(録音開始)
  2. ”録音を終了”ボタンを押す(録音を終了し、声のデータを生成。話者識別APIに生成した声のデータを投げて識別結果を取得、識別結果をサーバー側へ送り、画面遷移) 上記で実装出来ないかを考えております。

(仮)あなたの声の成分

<%= javascript_pack_tag 'voice_recording_vue' %>

app/javascript/packs/voice_recording_vue.js

new Vue({ el: '#app', data: {

// ① 変数を宣言する部分 status: 'init', // 状況 recorder: null, // 音声にアクセスする "MediaRecorder" のインスタンス audioData: [], // 入力された音声データ audioExtension: '' // 音声ファイルの拡張子

}, methods: {

// ② 録音を開始/ストップする部分
startRecording() {

  this.status = 'recording';
  this.audioData = [];
  this.recorder.start();

},
stopRecording() {

  this.recorder.stop();
  this.status = 'ready';

},

// ④ 音声ファイルの拡張子を取得する部分
getExtension(audioType) {

  let extension = 'wav';
  const matches = audioType.match(/audio\/([^;]+)/);

  if(matches) {

    extension = matches[1];

  }

    return '.'+ extension;

}

}, mounted() {

// ⑤ マイクにアクセス
navigator.mediaDevices.getUserMedia({ audio: true })
  .then(stream => {

    this.recorder = new MediaRecorder(stream);
    this.recorder.addEventListener('dataavailable', e => {

      this.audioData.push(e.data);
      this.audioExtension = this.getExtension(e.data.type);

    });

    this.recorder.addEventListener('stop', () => {

      const audioBlob = new Blob(this.audioData);
      fetch( 'https://westus.api.cognitive.microsoft.com/speaker/identification/v2.0/text-independent/profiles/identifySingleSpeaker?profileIds=34349224-8943-4073-8a42-e6bc6e35eb31,44b1405c-dc29-48c5-b916-cf344e2d26b3,94fa8cdd-afac-4c29-976d-44f766201e39&ignoreMinLength={true}', {
        method: 'POST',
        headers: {
          "Ocp-Apim-Subscription-Key": "****************************",
          "Content-Type":"audio/wav"
        },
        body: audioBlob
      }).then(response => {
        return response.json();

      }).then(data => {
        document.getElementById("result_data").value = JSON.stringify(data) ;
        console.log(JSON.stringify(data));

      }).catch(err => {
        console.log(err);
      })
    });
    this.status = 'ready';

    this.recorder.addEventListener('start', () => {
      setTimeout(function() {
        alert("stop");
        this.recorder.stop();
      }, 5000);
    });

  });

} });

app/controllers/identification_results_controller.rb

class IdentificationResultsController < ApplicationController skip_before_action :verify_authenticity_token

def identification_result result_data = JSON.parse(params[:hidden]) if result_data["identifiedProfile"] @profileId = result_data["profilesRanking"][0]["profileId"] @score = result_data["profilesRanking"][0]["score"] end end

end

yubachiri commented 3 years ago

タイミング的には「hiddenにvalueが設定されたらsubmitする」なので、クリック時ではなくそのタイミングでsubmitしたいんですね。

手元で試せていないので微妙ですが、

document.getElementById("result_data").value = JSON.stringify(data) ;

をした後で

document.getElementById("result_data")

と同じようなイメージで、documentからformを取得してきてsubmit()したら想定通りに動きますかね…? 参考: https://developer.mozilla.org/ja/docs/Web/API/HTMLFormElement/submit

あと、お困りのところと違う点についてですが、formから投げるリクエストがgetになっているのが気になりました。 こちらPOSTやPUT等ではなくあえてGETにしている理由って何ですか?

sayahaya commented 3 years ago

@yubachiri

返信遅くなり申し訳ありません!教えていただいた方法・参考サイトと自分で調べた参考サイトを元に下記実装し試してみたのですが、valueがnullになってしまいました・・・。実装方法が違うのでしょうか?

[参考サイト] https://www.sejuku.net/blog/28720

#app/javascript/packs/voice_recording_vue.js

          }).then(data => {
            document.getElementById("result_data").value = JSON.stringify(data) ;

            const result_data = document.getElementById("result_data").value;

            result_data.addEventListener('click', () => {
              document.myform.submit();
            });
            console.log(JSON.stringify(data));

          }).catch(err => {
            console.log(err);
          })
        });
        this.status = 'ready';
#app/views/static_pages/top.html.erb

  <div id="app">
      <!-- ③ 録音の開始/終了ボタンを設置する部分 -->
     <button class="btn btn-primary" type="button" v-if="status=='ready'" @click="startRecording">さっそく声をチェッ>ク</button>

    <form name="myform">
     <button class="btn btn-danger" type="button" id="result_data" value="" v-if="status=='recording'" @click="stopRecording;location.href='./identification_result'">録音を終了する</button>
    </form>

  </div>

あと、お困りのところと違う点についてですが、formから投げるリクエストがgetになっているのが気になりました。 こちらPOSTやPUT等ではなくあえてGETにしている理由って何ですか?

画面遷移先のルーティングを

get 'identification_result', to: 'identification_results#identification_result'

上記で記述しており、formのメソッドをPOSTで送信を実行するとルーティングエラーになってしまう為です。 自分の認識では

という認識だったので今回の画面遷移先はAPIで識別させた結果を表示するというページなのでGETの方が正しいのかなというと思いそのように実装しておりました。ただ自分もこの判断が正しいのかなと思っていたのでこちらの認識についても間違いなどあればご教示いただけると有難いです。

takyyk commented 3 years ago

自分もちょっと試せていないのですが、valueがnullになったところをもう少し詳細に教えていただきたいです!

document.getElementById("result_data").value = JSON.stringify(data) 

ここのJSON.stringify(data)のdataの部分がnullだからvalueもnullになってしまっているのかとかその辺りです! またもう一つの質問ですが、GETやPOSTはHTTP通信のリクエストの方法でサーバーへどうデータを送信するかというところなので

画面遷移先はAPIで識別させた結果を表示するというページ

と書かれていますが、結果を表示するかどうかというより、ここではFormからユーザーが入力したものはいつか保存する可能性もありうるためリクエストボディに入れてサーバーへデータを送信するのでPOSTが適切かな?と思います。

GETだとURL上にパラメーターとしてデータを渡して情報を取得する通信なので、画面遷移して表示させたり、パラメータに付与したデータで絞り込み検索をかけて表示させたりでより使われますね。

sayahaya commented 3 years ago

返信遅くなってしまい申し訳ありません!また長文失礼致します。

valueがnullになったところをもう少し詳細に教えていただきたいです!

この時の原因は#app/views/static_pages/top.html.erbの

<button class="btn btn-danger" type="button" id="result_data" value="" v-if="status=='recording'" @click="stopRecording;location.href='./identification_result'">録音を終了する</button>

上記の@click="stopRecording;location.href='./identification_result'の部分でクリック時にstopRecording(APIに識別させ結果データ取得)とlocation.href='〜(画面遷移)を同時に実行させていたためにvalueがnullになってしまっているのだと気付き、JS内でAPIに識別結果取得→documentからformを取得してきてsubmit→画面遷移を記述すれば想定通りに動くかと思い下記に変更してみました。しかし、

#app/javascript/packs/voice_recording_vue.js

          }).then(data => {
            document.getElementById("result_data").value = JSON.stringify(data) ;
              document.hidden.submit();
              window.location.href = './identification_result';
              console.log(JSON.stringify(data));

            }).catch(err => {
              console.log(err);
            })
          });
          this.status = 'ready';
#app/views/static_pages/top.html.erb

    <!-- ③  録音の開始/終了ボタンを設置する部分 -->
     <button class="btn btn-primary" type="button" v-if="status=='ready'" @click="startRecording">さっそく声をチェック</button>
    <button class="btn btn-danger" type="button" v-if="status=='recording'" @click="stopRecording">録音を終了する</button>

     <form action="./identification_result" method="get" name="hidden">
        <input type="hidden" id="result_data" value=""></input>
     </form>

しかし、この記載だと識別データ取得と画面遷移は出来るのですが、サーバー側に識別データが渡せずに下記エラーとなってしまいます。 エラー画像 教えていただいたdocumentからformを取得してきてsubmitという部分が上手く実装出来ていないのだと思うのですが、ここから先がまた分からなくなってしまい、恐れ入りますがアドバイスをいただけないでしょうか。

またもう一つの質問ですが、GETやPOSTはHTTP通信のリクエストの方法でサーバーへどうデータを送信するかというところなので 結果を表示するかどうかというより、ここではFormからユーザーが入力したものはいつか保存する可能性もありうるためリクエストボディに入れてサーバーへデータを送信するのでPOSTが適切かな?と思います。

こちら回答ありがとうございます!上記説明で自分の中で理解出来たのでPOSTが適切だと思い下記実装してみたのですが、

#app/views/static_pages/top.html.erb

    <form action="./identification_result" method="post" name="hidden">
       <input type="hidden" id="result_data" value=""></input>
     </form>
# config/routes.rb

 post  'identification_result', to: 'identification_results#identification_result'

下記のルーティングがgetになってしまいエラーが出てしまいます。 エラー画像2

初歩的なところを見逃している気がするのですが、こちらもアドバイスいただけないでしょうか。

takyyk commented 3 years ago

document.hidden.submit();が機能していないのでここを修正してどうにかsubmitしてparameterにJSON.stringify(data)の内容を送りたいという感じですね。

ここのdocumentdocument.getElementById("result_data").valueと一致していないのでsubmitしても何も送れていないのではないのかなーと思います。

2つ目の質問ですが、/identification_resultのリソースにアクセスしてしているのでGETと判断されているけどルーティングがないのでRouting Errorが出ていますね。 POSTの場合画面を描写するリクエストではないためエラーになっています!

sayahaya commented 3 years ago

2つ目の質問ですが、/identification_resultのリソースにアクセスしてしているのでGETと判断されているけどルーティングがないのでRouting Errorが出ていますね。 POSTの場合画面を描写するリクエストではないためエラーになっています!

ありがとうございます!こちらは内容理解でき、解決出来ました!

ここのdocumentがdocument.getElementById("result_data").value と一致していないのでsubmitしても何も送れていないのではないのかなーと思います。

documentにgetElementById("result_data")を付けてsubmitすれば送ることが出来ると思い下記のように実装しましたが、

            }).then(data => {
              document.getElementById("result_data").value = JSON.stringify(data);
              document.getElementById("result_data").value.submit();

今度は ’TypeError: document.getElementById(...).value.submit is not a function’ のエラーが出ており、 エラー画像 下記試してみましたが、エラー解消に至らずでした。

あとの原因はsubmit();の使い方が違う。。。とかだと思うのですが、そちらは調べた限り分からずという状況です。恐れ入りますがアドバイスをお願い出来ますでしょうか。

takyyk commented 3 years ago

submit();の使い方が違う

こちらの推測であってそうですね! submit()はFormElementに対して使うものなので、 document.getElementById("result_data").submit() ではどうなるでしょうか?

参考: https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit

sayahaya commented 3 years ago

submit()はFormElementに対して使うものなので、 document.getElementById("result_data").submit() ではどうなるでしょうか?

valueは付けないということですね!実行してみたのですが下記のエラーが出てきます

TypeError: document.getElementById(...).submit is not a function

エラー画像

takyyk commented 3 years ago

あ、すいません、submit()はFormに対して使えるメソッドなので、formにあたってるIdじゃないと使えないですね!

sayahaya commented 3 years ago

ありがとうございます!下記のようにformに新たにidを設定してsubmit()をしたところ、TypeError: document.getElementById(...).submit is not a function はなくなり画面遷移出来ました!

#app/views/static_pages/top.html.erb

      <form action="./identification_result" method="post" id="data">
        <input type="hidden" name="hidden" id="result_data" value=""></input>
      </form>
#app/javascript/packs/voice_recording_vue.js

            }).then(data => {
              document.getElementById("result_data").value = JSON.stringify(data);
              console.log(JSON.stringify(data));
              document.getElementById("data").submit();
              window.location.href = './identification_result';

            }).catch(err => {
             console.log(err);
            })

ただデータが上手く送れずparamsが空になってる状態です。恐らくの予想としてsubmit()のところの問題ではなく以前アドバイスいただいた

2つ目の質問ですが、/identification_resultのリソースにアクセスしてしているのでGETと判断されているけどルーティングがないのでRouting Errorが出ていますね。POSTの場合画面を描写するリクエストではないためエラーになっています!

このあたりが理解出来ておらずデータが送れてないのかなと思うのですが。。。アドバイスを元にPOSTでcreateアクションにデータを渡してそこから画面遷移先に渡せばいいのかと思い下記実装にしたのですが、認識違いますでしょうか?

エラー画像

#ルーティング

   get  'identification_result', to: 'identification_results#identification_result'
   post 'identification_result', to: 'identification_results#create'
#コントローラー

   def create
     @result_data = JSON.parse(params)
   end

    def identification_result
     if @result_data["identifiedProfile"]
       @profileId = result_data["profilesRanking"][0]["profileId"]
       @score = result_data["profilesRanking"][0]["score"]
     end
   end
takyyk commented 3 years ago

今はGETで/identification_resultのリソースを取得していますが、@result_data["identifiedProfile"]がnilのため、うまくいっていませんね。 まずcreateアクションの方で正しくデータを受け取ってPOSTできるかどうかを試してみましょう。 JSのsubmit()でresult_dataをparameterとしてサーバー側に渡す→createアクションで処理する(保存する)→保存したものを画面描写先で取り出す という流れはよさそうかなと思います。

まずcreateアクションの方がうまくいっているか確認してみてください。