LoginSignup
18
19

More than 5 years have passed since last update.

ついまるが1.1対応しないなら自分で作ればいいじゃない

Posted at

はじめに

この記事は社内 Advent Calendar のために書いたものをついでに一般に公開するものです。
ネタを探してたらちょうど11月に趣味で作ったものが色々なキーワードに触れられるものだったので
「これでいこう!」と相成りました。

調子に乗って長くなったので胃もたれるする人もいるかもしれません。
次のキーワードのうち興味がある部分だけ読むのもそれはそれでアリかなーと。
テレビのCMくらいのつもりでつまみ読みしてみてください。
記事自体もCMくらいの情報量しかないです。詳しくはWebで!

  • 音声合成エンジン
  • Team Foundation Service
  • NuGet
  • OAuth 認証
  • async/await
  • ReactiveExtensions

きっかけ

皆さん「ついまる」ってご存じですか?

こちらの動画を見ていただくとすぐわかりますが、要はツイッターのタイムラインを音声で読み上げてくれるおもちゃです。

私の家にはこれがあり、文字通り「ツイートの流れる家」だったのですが、6月のTwitter API変更によって使えなくなってしまいました。

ちょうど6月に引っ越しをした関係で色々バタバタしていて、しばらくはこいつの存在も忘れていました。
ところが久しぶりにツイートが流れるあの感じが恋しくなり「ついまるが1.1対応しないなら自分で作ればいいじゃない」と思いたちツイート読み上げアプリを作成してみた次第です。

作成してみたなかで、思いのほか色んなキーワードに広く浅く触れることが出来たので備忘も兼ねて記事にしてみます。記事の長さの関係上、それぞれのキーワードについては詳しく説明出来なかったので「各キーワードはどんなことができるのか」を知れる程度の「きっかけ作り」だと思って頂ければいいかなと思います。

まずは音声エンジンを選ぶ - はるかタンのインストール

何はともあれ「つくろう!」と思った時に最初に気になったのが「どうやってツイートを読み上げさせるの?」でした。

色々調べてみた結果、Microsoft から提供されている「Haruka」が良さげ。ということで下記の記事を参考にインストール。

手順としては

  1. 2つ目の記事を参考に「Microsoft Speech Platform 簡単インストーラー Ver.2.2」をダウンロードして実行
  2. 3つ目の記事を参考にレジストリキーを作成

となるのですが、注意するのが Haruka のバージョンの違い。
紹介記事では 10.0 となっていますが 2013年11月 時点で最新をインストールすると 11.0 になる模様。必要に応じてレジストリの書き換えが必要です。

  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech\Voices\Tokens\ 以下
  • HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Speech\Voices\Tokens\ 以下(64bitの場合こちらも)

私は2つ目のキーをきちんと見ていなくてハマりました。正常にインストール出来た場合は以下で確認ができます。

  1. コントロールパネルを開く
  2. [音声認識] の [音声合成] を開く Image 1.png
  3. [音声の選択(V)] の中に [Microsoft Server Speech Text to Speech Voice (ja-JP, Haruka)] が表示され、かつ選択したときに音声が再生されればOK Image 2.png

ところで、ささらたんはプログラムから使用すると規約違反になるので使えないんですよね。残念。

喋らせてみよう - TFService に Hello World をつくろう

さて、インストールが済んだらまずは喋らせてみたいですよね?ね?

ということで新規にプロジェクトを作成します。が、やっぱりバージョン管理はしたいですよね!途中で変な変更しちゃって壊しちゃったときにショックですし。試行錯誤が必要なこういうプログラミングではバージョン管理システムは必須です。

ここでは Team Foundation Service (以下 TFService) を使用します。こちらは Microsoft から提供されているクラウドの Team Foundation Server で5名まで無償で利用できます。(2013年11月時点)

まだアカウントをお持ちでない方は是非とも登録しましょう。

アカウントが登録できたら新規にプロジェクトを作成します。今回は Windows Form を選択しました。

Image 3.png

たぶん、本当はWPFを選んどいたほうが後々いいです。読み上げ対象としてリストを選ばせたいときなんかにリスト一覧表示するのとか、絶対WPFのほうがやりやすいので。が、記事の流れとか色んな大人の事情で今回はFormで。

名前の「Twingle」は「ついまる」をもじって「あっちが丸ならこっちは三角で」くらいのもんです。

作ったら、あとは先程の 無料で日本語音声合成 - Qiita [キータ] の通りのサンプルコードで読み上げが完了です。

MainForm.cs
using System;
using System.Speech.Synthesis;
using System.Windows.Forms;

namespace Twingle
{
    public partial class MainForm : Form
    {
        private const string Haruka =
              "Microsoft Server Speech Text to Speech Voice (ja-JP, Haruka)";

        public MainForm()
        {
            InitializeComponent();
        }

        protected override void OnShown(EventArgs e)
        {
            base.OnShown(e);

            var synthesizer = new SpeechSynthesizer();
            synthesizer.SelectVoice(Haruka);
            synthesizer.Speak("読み上げのテスト");
        }
    }
}

たった3行のソースコードで好きな言葉を喋らせることができるなんて今の技術は凄いですね!

Twitter 認証しよう - NuGet と OAuth 認証

さて、あとはタイムラインを取得できればツイートを喋らせることが出来そうです。

今回は Twitter アクセスのために TweetSharp というライブラリを使用しましょう。
NuGet を使えばすぐにインストールできます。

NuGet とは

Visual Studio 2012 から標準搭載されたライブラリ管理の仕組みです。(VS2010では拡張機能でインストール可能)
詳しくは @IT 辺りを参照してくださいw

参照設定を右クリックして [NuGet パッケージの管理] を選択。検索ボックスに [TweetSharp] と入れれば検索できます。

NuGet.png

TweetSharp.png

慣れないうちは左側で [オンライン] を選択し忘れることが多いのでご注意を(私ですが)

さて、参照できたらまずは認証が必要です。Twitter で外部アプリを使ったことがある人は経験したことがあると思いますが、[認証する]的なボタンを押したあとに出てきた数字を入力するアレです。

TwitterOAuth.png
TwitterVerify.png

試しに TweetSharp の TwitterService クラスのインスタンスを生成して、いかにも認証しそうなメソッドをインテリセンスから見つけだしてみます。

AuthenticateWith.png

ふむ。どうやら 4 つの string が必要なようです。

ということで、OAuth 認証の流れとしては次のような形になります。

  1. まず Twitter の開発者用ページで今回のアプリを登録します。すると consumerKeyconsumerSecret が手に入ります。この2つの文字列が Twitter に自分のアプリを特定してもらうためのパスワードのようなものになります。
  2. 次に、この 2 つのキーを使用してユーザー(アプリを使う人)に利用を許可してもらう必要があります。そうすることで tokentokenSecret が手に入ります。前 2 つの文字列と合わせて 4 つを Twitter に提示することで「どのアプリに対して」「どのユーザーが」サービスの利用を許可しているかが証明されます。

2 番について、更に細かい流れは以下のようになります。

  1. consumerKeyconsumerSecret を提示して GetRequestToken メソッドで一時的なトークンを取得する。
  2. 1 で得たトークンを GetAuthenticationUrl メソッドに渡して認証用のURLを得る。
  3. 2 で得たURLをユーザーに表示して認証ボタンを押下してもらう。
  4. 3 で表示された7桁の verifier を 1 で得たトークンとともに GetAccessToken メソッドに渡して tokentokenSecret を得る。

めんどくさそうですね。まぁやってみましょう。

まずは Twitter 開発者のページ でアプリを登録します。ちょっとわかりづらいですが右上の自分のアイコン(まだログインしていない場合はログインボタンがあるのでログインします)をホバーしてメニューから [My Applications] を選ぶと アプリ一覧のページ に飛びます。そこから [Create a new application] で登録を済ませます。

Consumer key、Consumer secret が手に入ったと思います(人に漏らさないでね!)

あとは前述の流れの通り。次のようなコードで認証画面を表示できます。
ConsumerKeyConsumerSecret はコンストラクタで渡すように変更してます)

protected override void OnShown(EventArgs e)
{
    base.OnShown(e);
    var twitter = new TwitterService(ConsumerKey, ConsumerSecret);
    var requestToken = twitter.GetRequestToken();
    var uri = twitter.GetAuthorizationUri(requestToken);
    Process.Start(uri.ToString());
}

Process.Start(uri.ToString()) してるのは関連付けに任せて標準のブラウザでページを開かせるためです。WebBrowser コントロールを配置してそちらに表示するのでもOKです。お好きな方で。

ユーザーに認証してもらったら何らかの形(テキストボックスに入力してもらってボタンを押してもらうとか)で verifier を受け取ります。

private void btnOK_Click(object sender, EventArgs e)
{
    var accessToken = _twitter.GetAccessToken(_requestToken, txtVerifier.Text);
    Console.WriteLine(accessToken.Token);
    Console.WriteLine(accessToken.TokenSecret);
    Console.WriteLine(accessToken.UserId);
    Console.WriteLine(accessToken.ScreenName);
}

_twitter_requestToken はフォームのフィールドに格上げしました。

さて、一度認証したら次からはもう認証なんてすっ飛ばしたいので tokentokenSecret はローカルに保存することになるでしょう。すると次からはユーザーにボタンなど押してもらう必要はなくなります。

さて、困ったのが

  • 初回起動の際はユーザーにボタンを押して貰うまでタイムラインの取得ができない
  • 2回め以降の起動ではフォーム起動とともにタイムラインの取得を開始できる

これらをどうやってうまく書くか。

「ユーザーにボタンを押して貰うまで」というのはいわゆるコールバックが必要ということで、実はこれは一種の非同期動作と捉えることができます。そこで!C#5.0の新機能 async/await の登場です。(*1)

同期操作と非同期操作を一緒に書こう! - async/await

結論から提示すると、TaskCompletionSource<T> というクラスを利用して次のように書くことができます。

protected override async void OnShown(EventArgs e)
{
    base.OnShown(e);

    _twitter = new TwitterService(ConsumerKey, ConsumerSecret);

    // ローカルに保存した accessToken を読み込む(無ければ null)
    var accessToken = LoadAccessToken();
    // 認証がまだなら認証する
    if (accessToken == null)
    {
        // GetAccessToken メソッドはユーザーがボタンを押下するまで非同期的に待機される
        accessToken = await GetAccessTokenAsync();
        // 一度取得した accessToken はローカルに保存しておく
        SaveAccessToken(accessToken);
    }

    _twitter.AuthenticateWith(accessToken.Token, accessToken.TokenSecret);

    // タイムラインを読み込む処理
}

private Task<OAuthAccessToken> GetAccessTokenAsync()
{
    var source = new TaskCompletionSource<OAuthAccessToken>();
    _requestToken = _twitter.GetRequestToken();
    var uri = _twitter.GetAuthorizationUri(_requestToken);
    Process.Start(uri.ToString());

    btnOK.Click += (sender, e) =>
    {
        var verifier = txtVerifier.Text;
        var accessToken = _twitter.GetAccessToken(_requestToken, verifier);
        source.SetResult(accessToken);
    };

    return source.Task;
}

jQuery.Deferred を使用したことがある人は次のように対応づけて理解してください。

  • TaskCompletionSource<T> : Deferred
  • SetResult : resolve
  • Task : Promise

(*1) ところで、これを作成したあと「認証終わってからもメイン画面にテキストボックスとボタンがあるのは変だな…」と思って別フォームにしたのですが、そうすると ShowDialog でモーダルに表示してやるだけで await とか要らなくなるんですよね。なんとアホな…

ユーザーストリームを取得しよう! - Reactive Extensions

さて、長い戦いですが認証が終わればあとはタイムラインを取得して喋らせるだけ!

と、ここで「ユーザーストリーム」について触れておきましょう。

Twitter ではタイムラインやリストの内容を取得するためにリクエストを送信して取得する普通の方法、すなわち Pull式 の方法の他に、Twitter サーバーと接続を開きっぱなしにすることでリアルタイムにツイートを受信する方法、すなわち Push式 の方法があり後者をユーザーストリームといいます。

ユーザーストリームではリストの内容は受信できず、あくまでフォローしているユーザーのタイムラインしか取得できないのですが、せっかく喋らせる以上はリアルタイムがいいですよね!

ということでユーザーストリームで届いたツイートのうち、特定のリストのユーザーによる呟きだけをフィルタして喋らせることにしましょう。

(あ、言ってなかった気がしますがこれが今回の要件です。フォロー数900人超えで本当にタイムラインをそのまま喋らせると家の中がうるさくてたまらないのです)

TweetSharp の素の使い方で書くなら以下のようになります。

// 読み上げ対象にしたいリストのメンバーを読み込む処理。これが面倒だけど解説は省略。。。
var members = GetMembers();
// ユーザーストリームの購読を開始。Push 式なのでコールバックを渡す形になる。
_twitter.StreamUser((artifact, response) =>
{
    // ユーザーストリームでは「お気に入りに登録された」のような情報も流れてくるので
    // 種類で分岐する必要がある
    // TwitterUserStreamStatus は呟きがタイムラインに流れてきた場合 (A)
    if (artifact is TwitterUserStreamStatus)
    {
        var status = (TwitterUserStreamStatus)artifact;
        // 対象メンバーの場合だけ読み上げる (B)
        if (members.Contains(status.Author.ScreenName))
        {
            // 「あっとまーく誰々 ツイートの内容」みたいに読み上げさせたい
            // 「あっとまーくしょこらみんと おなかすいたなぁ」みたいな (C)
            var text = String.Format("@{0}: {1}",
                                     status.Author.ScreenName,
                                     status.Text);
            synthesizer.SpeakAsync(text);
        }
    }
});

うーん、イケてない。

そもそも「たくさんの要素を特定の型で絞ってキャストして(A)、特定の条件の場合だけ(B)特定の形に変換して(C)何かする」って何かきいたことある流れじゃないですか?

そう、これ普通だったら LINQ で書けるよ!
だってもう .TypeOf<T>().Where().Select() そのまんまですよね。LINQ で書きたいですよ。ね?
しかし残念ながらこれは IEnumerable<T> にはできません。だって foreach で回そうにも各要素が取得できるタイミングって呟く側に依存するわけで。要は Pull じゃないんです。Push なんです。

これが Pull 式だったら LINQ で書けるのに…
Push 式のシーケンスにも LINQ があればなぁ…
え?あるの?マジで?

ということで Reactive Extensions を使います。Reactive Extensions についてここで詳しく解説し始めるとキリがない(というか私そんな詳しくない)ので C# で有名なあの人とかあの人の解説をご覧ください。

で、要は先ほどのイケてない StreamUser メソッドを IObservable<T> にしてあげればいいのでこんな感じになります。(Reactive Extensions 自体は NuGet からインストールしてください。Main Library でいいです)

// こんな拡張メソッドを用意してやれば…
public static class TwitterServiceExtensions
{
    public static IObservable<TwitterStreamArtifact> StreamUser(this TwitterService twitter)
    {
        return Observable.Create<TwitterStreamArtifact>(observer =>
        {
            twitter.StreamUser((artifact, _) => observer.OnNext(artifact));
            return twitter.CancelStreaming;
        });
    }
}
// こうなります\(^o^)/すっきり!
_twitter.StreamUser()
        .OfType<TwitterUserStreamStatus>()
        .Where(x => members.Contains(x.Author.ScreenName))
        .Select(x => String.Format("@{0}: {1}", x.Author.ScreenName, x.Text))
        .Subscribe(x =>
        {
            synthesizer.SpeakAsync(x);
        });

なんということでしょう…
匠の力により面倒な if 文の入れ子はメソッドチェーンに。やりたいことが直感的に書けています。
さすがLINQ!おれたちにできない事を平然とやってのけるッ そこにシビれる!

ということで、あとは細かく色々自分の要望に合わせて機能を拡張していくだけです。

 おわりに

今回このアプリを作ってみて総じて思ったことは「 .NETすげぇ! 」でした。
これだけのことがこんなに簡単に実現できる.NETはきっと特別な存在です。
そしてそれを支えているのがC#という言語ですね!F#でも同じくらい簡単にいけそうですけどね!
.NET言語もすごいですね!
(それと Visual Studio & ReSharper も!PHP を echo デバッグしてた時代が嘘のよう!)

ということで長々と書かせて頂きましたが、何か1つでも参考になる内容があれば幸いです。
それでは今後とも豊富なライブラリと強力な言語機能を使った良き.NETライフを!
ここまで読んで頂きありがとうございました!

18
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
19