WPFで Web Speech API を使ってみよう!

クレウスさんの、

なところから、Web Speech API がブラウザ上で使えることが分かったので試してみるの巻です。

Web Speech APIの実装 – Speech Synthesis API | CodeGrid https://app.codegrid.net/entry/2016-web-speech-api-1

ブラウザ上で Web Speech API を使って文章の読み上げができる、って話は聞いてことはあったのですが、これ OS の機能をブラウザから呼び出している訳ではなくて、ブラウザの内部機能として実装されている、という話だったのですね。なので、Google Chrome で読み上げると Google の音声で喋るし、Edge で動かすと Microsoft の音声出力になります。おそらく Safari とかも違う音声になっているはず。

Javascript は簡単

読み上げするだけなら非常に簡単で、

var text = “hello world.”;
speechSynthesis.speak(new SpeechSynthesisUtterance(text));

と Javascript で書くだけです。text の部分は、文章を選択した場所でもよいし、何かを喋らせたい文章を書くもよし。声質も変えられるので、自前であれこれできます。

じゃあ、.NET から使ってみよう

ブラウザ上で音声を出せることは分かったけど、では、.NET からどう扱えばいいのか?ってことを考えると悩みどころなのですが、まあ、定番の WebBrowser か WebView を使えばなんとかなるでしょう?と考えました。

が、試してみると、意外と難関があることが判明。

  • WinForms や WPF で使われている WebBrowser は内部で IE を使っているので、Web Speech API の対象にならない。

なので、確か WPF などで Edge エンジンを使えるようになったような気がしたので、調べなおしてみると、

WPFやWindowsフォームでEdgeのWebViewを使うには?[Windows 10 1803以降]:.NET TIPS – @IT
http://www.atmarkit.co.jp/ait/articles/1807/04/news017.html

に、ありますね。

どうやら、NuGet で「Windows Community Toolkit v3.0」を落としてきて、その中にある WebView を使えばよいそうです。Microsoft.Toolkit.Win32.Controls を NuGet でダウンロードします。ちなみに、このコントロールは .NET Framework 4.6.2 以上なので、別途 .NET Framework をダウンロードする必要があります。開発者向けのをダウンロードしないといけないので、https://docs.microsoft.com/ja-jp/dotnet/framework/install/guide-for-developers から新しいものを入れてください。

image

ツールボックスに「WebView」コントロールが増えるので、これをウィンドウに貼り付けます。

image

読み上げるための TextBox と Button も貼り付けておきます。

image

グレーの WebView は、呼び出し用にあるだけなので見えなくても大丈夫なはず。大抵の例は、ブラウザ上で Web Speech API を動かすわけですが、ここでは WPF に貼り付けてある TextBox を読み上げるようにします。

 

InvokeScript で Javascript を実行する

ブラウザ内にある Javascript を実行するのに、WebView には InvokeScript というメソッドがあります。WebBrowser のときには、WebBrowser.Document.InvokeScript だったのですが、WebView では Document がなくなっていて、直接 InvokeScript メソッドが追加されていますね。Document がなくなったのでブラウザの DOM を直接見ることはできなくなったのですが、まあ、InvokeScript があればトリッキーなことをして DOM を探ることもできるので問題はありません。

以下、は動作コードですが、いつか試行錯誤の痕跡を残しておきます。

public partial class MainWindow : Window
 {
     public MainWindow()
     {
         InitializeComponent();
         this.Loaded += MainWindow_Loaded;
     }
     private void MainWindow_Loaded(object sender, RoutedEventArgs e)
     {
         // web.IsJavaScriptEnabled = true;
         // web.Navigate("http://moonmile.net/speech.html");
         // 非推奨だがローカルファイルから読み込む
        web.NavigateToLocal("speech.html");
     }

    private void clickGo(object sender, RoutedEventArgs e)
     {
         var text = @"
<body>
<script>
function hello(text){
  speechSynthesis.speak(new SpeechSynthesisUtterance(text));
}
</script>
</body>
 ";
         // 直接文字列を入れるとダメ
        // web.NavigateToString(text);
         this.web.InvokeScript("hello", new string[] { text1.Text } );
     }
 }

NavigateToString メソッドを使って、直接 HTML を入れることはできるのですが、なぜか InvokeScript メソッドを呼び出したときにアプリケーションごと落ちてしまいます。仕方がないので、Navigate メソッドで外部サイトから読み込むか、NavigateToLocal メソッドでローカルファイル(exeと同じフォルダ)から読み込みます。NavigateToLocal は非推奨ってことになっていますが、まあ、そのまま使えます。InvokeScript メソッドの呼び出しで落ちるのは、何かの初期化が足りていないんでしょう。

InvokeScript メソッドは、HTML 内にある Javascript の関数を直接呼び出せます。パラメータも渡すことができるので、これを TextBox から取り込みます。

ローカルファイルにある speech.html の内容は、以下のように hello 関数にひとつの引数を入れたものだけです。

<body>
<script>
function hello(text){
  speechSynthesis.speak(new SpeechSynthesisUtterance(text));
}
</script>
</body>

後は、ボタンを押して InvokeScript を呼び出せばふつうに Edge で喋ってくれます。

これは Unity で使えるのか?

さて、肝心のこの仕組みは Unity で使えるんでしょうか?って話なんですが、たぶん、ダメだと思うんですよね。UWP だと使えるんでしたっけ?

と思って、UWP で使ってみると…あっさりいけますね。UWP のほうはもともと内部的に Edge が使われているので大丈夫そうです。NavigateToString メソッドも普通に使えます。InvokeScript はダメで、InvokeScriptAsync だとうまく動きます。

public sealed partial class MainPage : Page
 {
     public MainPage()
     {
         this.InitializeComponent();
         this.Loaded += MainPage_Loaded;
     }

    private void MainPage_Loaded(object sender, RoutedEventArgs e)
     {
         // this.web.Navigate(new Uri( "http://moonmile.net/speech.html"));
         var text = @"
<body>
<script>
function hello(text){
  speechSynthesis.speak(new SpeechSynthesisUtterance(text));
}
</script>
</body>
 ";
         this.web.NavigateToString(text);
     }

    private async void clickGo(object sender, RoutedEventArgs e)
     {
         await web.InvokeScriptAsync("hello", new string[] { text1.Text });
     }
 }

ってことは、Unity でもいけるのかも。

~~~
追記 2018/10/01

Unity はやってないのでわからないのですが、Xamarin.Forms と Xamarin.Android の WebView で試してみました。結果は「ダメ」です。Android 上の Chrome では動くのですが、WebView の内部で使っている Chrome(Chroniumらしい)が対応していないらしく、AspeechSynthesis を認識できません。ちなみに音声を出すだけならば、TextToSpeech があるので、それを使えば ok. iOS の WebView の場合は AspeechSynthesis が使えるようです。

カテゴリー: C# パーマリンク