Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
23
Help us understand the problem. What is going on with this article?
@hoshimado

HTMLとJavaScriptだけで音声入力

More than 3 years have passed since last update.

初めまして。
プログラマもすなるQiitaというものを(以下略)。

Google Chrome限定だけど、HTMLとJavaScriptだけで音声入力が実現できてしまうらしい。
というわけで、作ってみた。

この記事は、スマホとタブレット上でのGoogle Chrome にて、
音声入力して<input type="text">にテキスト入力するのが目的。
テキスト取得までをクラス(はJavaScriptでは無いけれど)化して、
以下のように簡単に呼び出すのが目的。
(※今回のは、自サイトで公開済みの内容と一部被ります)

speech.html
speech_input = new SpeechInputWs( null );

// 録音開始ボタンの動作
$("#id_start_button").click(function(){
    var dfd = speech_input.start();
    dfd.done( function( text ){
        $("#id_output").val( text );
    });
});

AndroidスマホとタブレットのChrome 47で動作確認。

参考にさせていただいたのは、以下のサイト様。
http://qiita.com/inouet/items/2c9e218c05f547bb6852
http://www.cyokodog.net/blog/web-speechi-api/

SpeechInputWs() クラス(はJavaScriptでは無いけれど)の、
実装は下記。
(ところで、JavaScriptにおいて
 こういうクラスっぽいものを、なんと呼称すればよいのだ?)

SpeechInputWs()で、やってることは、
参考にしたサイト様( http://qiita.com/inouet/items/2c9e218c05f547bb6852 )の
コードをラッパーして、
音声取得中のオーバーレイdivで「音声入力中・・・」表示を行い、
完了したら候補を5個まで表示して、
選択されたradioボタンにてテキストを確定して、
jQueryのdeferred.done( function(text){} )の形式で返す、って内容。
「音声入力中・・・」の通知を出すdivも自前で描画。

speech_input.js
/*
    [speech_input.js]
    encoding="UTF-8"
*/


// Googleの音声入力用のクラス定義
//   ref. http://qiita.com/inouet/items/2c9e218c05f547bb6852
//   ref. http://www.cyokodog.net/blog/web-speechi-api/
//   ref. https://dvcs.w3.org/hg/speech-api/raw-file/tip/speechapi.html 【公式Spec】
// 
// noticeOverlay = null指定可能。その場合は、通知用divを自動生成。
// ,start() で、deferred が返るので、.done( text )で取得して任意に設定する。
//
// ※音声入力の実行可能不可能の自前判定は無いので、別途「スマホ/タブレットのみOK」などの
//  処理を実装の事。
//
var SpeechInputWs = function( noticeOverlay ){
    var self = this;
    this.itsNoticeOverlayDiv = noticeOverlay;

    // 通知用のオーバーレイdivが無ければ自前で生成する。
    if( !this.itsNoticeOverlayDiv ){
        $("body").append( "<div id=\"_id_overlay_notice\">hoge</div>" );
        this.itsNoticeOverlayDiv = $("#_id_overlay_notice");
        this.itsNoticeOverlayDiv.css({
            "display" : "none",
            "width"   : "280px",
            "height"  : "160px",
            "text-align" : "left",
            "position"   : "fixed",
            "top"  : "24px",
            "left" : "24px",
            "z-index" : "100",
            "background-color": "#cccccc"
        });
    }

    // 一応、毎回初期化???
    window.SpeechRecognition = window.SpeechRecognition || webkitSpeechRecognition;

    // オブジェクト生成
    this.itsRecognition = new webkitSpeechRecognition();
    this.itsRecognition.lang = 'ja';
    this.itsRecognition.maxAlternatives = 3;

    // 終了時に呼び出されるメソッドを設定
    this.itsRecognition.addEventListener("result", function(event){
        // この場面でのthisはグローバル扱いの可能性があるので、クロージャーのselfを経由しておく。
        self._selectTextFromSpeechResult( event.results );
    });
    this.itsLastDfd = null;
};
SpeechInputWs.prototype._selectTextFromSpeechResult = function( speechResult ){
    // ※予め、this.itsLastDfd にDeferred::promise() が入っている事。
    var dfd_notice = this.itsLastDfd;
    var candidate = speechResult.item(0);
    var i = candidate.length;
    var str;

    if ( i == 0 ){ 
        dfd.resolve( candidate.item(0).transcript );
        return dfd.promise();
    }

    // 【FIXME】あれ? .maxAlternatives=3 が効いてない???
    if( i > 5 ){ i = 5; }

    // 候補を表示してユーザー入力を待つ。
    str = ""; // 下記で作成済みのformへ追加するスタイル。
    while( 0 < i-- ){
        str += "<div><input type=\"radio\" name=\"candidate\" value=\"" + candidate.item(i).transcript + "\">";
        str += candidate.item(i).transcript;
        str += "</input></div>";
    }
    this.itsNoticeOverlayDiv.find("form").eq(0).append( str );

    this.itsNoticeOverlayDiv.find( "form div" ).each( function(){
        $(this).click( function(){
            dfd_notice.resolve( $(this).find("input").eq(0).val() );
        });
    });
    this.itsNoticeOverlayDiv.find( "form input[type='button']" ).each( function(){
        // 動作を変更する(stop()処理が不要になる。また投げる先のdfdが異なる)。
        $(this).click( function(){
            dfd_notice.reject( null );
        });
    });

    // 戻り値なし。
};
SpeechInputWs.prototype.start = function() {
    var self = this;
    var dfd_sound = new $.Deferred();
    var str = " <form>音声入力中・・・ &nbsp; <input type=\"button\" value=\"中止\"></input><br>";
    str += "</form>";

    // 音声入力の受付を開始
    this.itsRecognition.start(); 

    // ユーザー通知系の処理
    this.itsNoticeOverlayDiv.empty();
    this.itsNoticeOverlayDiv.append( str );
    this.itsNoticeOverlayDiv.show();
    this.itsNoticeOverlayDiv.find("form input[type='button']").each( function(){
        $(this).click( function(){
            self.itsRecognition.stop();
            self.itsNoticeOverlayDiv.hide();
            dfd_sound.reject( null );
        });
    });

    // 録音終了時トリガーを受けるDeferredを設定
    this.itsLastDfd = new $.Deferred();
    this.itsLastDfd.done( function( text ){
        dfd_sound.resolve( text );
    }).fail( function(){
        dfd_sound.reject( null );
    }).always( function(){
        self.itsNoticeOverlayDiv.hide();
    });

    return dfd_sound.promise();
};

※なお、「スマホ/タブレットからのアクセスか?」で判定して音声入力ボタンを有効/無効で切り替えるとよいかも? 当方の場合、アクセス判定には https://w3g.jp/blog/js_browser_sniffing2015 のコードを利用させてもらっている。サンクス!
※何も考えずに、new new SpeechInputWs( null );のところで、try{}catch (e){}するだけで、実行可能環境か否かの判定できた(2016.06.03追記)。何で最初は上手く判定できなかったんだろう?

23
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
hoshimado
趣味で日曜プログラミング。仕事はたぶんIT関連? この2016年春から、ふとJavaScript周りに興味が沸いたので、Webアプリベースで「自分が便利」ツール作成しつつ、その裏で「コピペで使えるコード」の共有と勉強を目的にQiitaに参戦。https://twitter.com/hoshimado7

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
23
Help us understand the problem. What is going on with this article?