C#でTwitterのタイムラインを東北きりたんに読み上げてもらった

#はじめに
[TwitterのTLをボイスロイドに読み上げてもらう](https://qiita.com/MichaelAsterisk/items/7e71730ad948c11fe27d)
この記事をまねてPythonでTwitterのタイムラインを読み上げてもらってたがTweepyではMuteメソッドを扱ってなく、Friendshipの方もエラーをはいて取得できなかったのでCoreTweetを用いてC#で作ろうと思った。
ついでにGUIのクライアントみたいなものも作ろうと思った。
ついでに民安★TALKも経由せずに読み上げさせようと思った(一応民安★TALK経由バージョンも書いときます)
あとWindows FormsとWindows Presentation Foundationの2通りで作ったんでそれについてもちょっと触れます。

C#を今まで触ったことのない人間が作ったので色々変な部分があると思います。

#実行環境
* Windows7
* VOICEROID＋ 東北きりたん

#準備
NuGet経由で必要なパッケージを入れる

###TwitterAPI用のパッケージ
* CoreTweet
* System.Reactive.Linq

###VOICEROID読み上げに必要なパッケージ(民安★TALKを使う場合は必要なし)
* Codeer.Friendly
* Codeer.Friendly.Windows
* Codeer.Friendly.Windows.Grasp
* Codeer.Friendly.Windows.NativeStandardControls
* Ong.Friendly.FormsStandardControls
* RM.Friendly.WPFStandardControls


#コード
重要だと思った部分だけ抜きだして書きます


##Twitterタイムラインの取得
[ストリーミングAPIを使う・CoreTweet/CoreTweet Wiki](https://github.com/CoreTweet/CoreTweet/wiki/%E3%82%B9%E3%83%88%E3%83%AA%E3%83%BC%E3%83%9F%E3%83%B3%E3%82%B0-API-%E3%82%92%E4%BD%BF%E3%81%86)
[C#でTwitterアプリを作る 第3回 Streaming](http://laco0416.hatenablog.com/entry/2014/03/25/201839)


```cs
/* tokensの作成 */
CONSUMER_KEY = "";
CONSUMER_SECRET = "";
ACCESS_TOKEN = "";
ACCESS_SECRET = "";
tokens = CoreTweet.Tokens.Create(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_SECRET);


var stream = tokens.Streaming.UserAsObservable().Publish();

Action<StatusMessage> printStatus = (message) =>
{
    var status = (message as StatusMessage).Status;
    /* 取得したツイートの処理 */
}

stream.OfType<StatusMessage>().Subscribe(printStatus);

/* streamingを管理する変数connection */
connection = stream.Connect();
```

タイムラインを止めるにはこのconnectionを使って

```cs
connection.Dispose();
```

すればいい。

##VOICEROIDに読み上げてもらう(民安★TALK経由)
###VOICEROIDを起動する
System.DiagnosticsのProcessStartInfoを使う。

```cs
/* ボイスロイドのパス */
PATH_VOICEROID = @"...\VOICEROID.exe";

vr = new ProcessStartInfo();
vr.FileName = PATH_VOICEROID;
vr.CreateNoWindow = true;
vr.UseShellExecute = false;

/* VOICEROIDの起動 */
Process.Start(vr);
```

同様に民安★TALKも起動してあげる。
あとは[[TwitterのTLをボイスロイドに読み上げてもらう](https://qiita.com/MichaelAsterisk/items/7e71730ad948c11fe27d)]と同様に
ツイートの処理タイミングで民安★TALKの引数に読み上げる文章を設定し、Process.Startしてあげればいい。

```cs
PATH_VRX = @"...\VRX.exe";

vrx = new ProcessStartInfo();
vrx.FileName = PATH_VRX;
vrx.CreateNoWindow = true;
vrx.UseShellExecute = false;
/* 引数に読み上げる文章を渡してあげる */
vrx.Arguments = ”読み上げてほしい文章”;

/* 読み上げる */
Process.Start(vrx);
```

##VOICEROIDに読み上げてもらう(民安★TALK非経由)
CodeerのLogicalTreeが上手く動かず悩んでたところ
SeikaCenterのコードにVOICEROIDの解析結果があるおかげで何とか動かすことができた ~~(というかほとんどそのまま)~~
[SeikaCenterの関係コード[努力したWiki]](https://hgotoh.jp/wiki/doku.php/documents/voiceroid/seikacenter/seikacenter-090)

VOICEROID+用の奴しかやってないんでVOICEROID2等はSeikaCenterのコードを見てください

まずはVOICEROIDを起動しプロセスからVOICEROIDのテキストボックス・再生ボタン・保存ボタンを取得する

```cs
//VOICEROIDのパスの割り当て
PATH_VOICEROID = @"...\VOICEROID.exe";
pname = "VOICEROID";
vr = new ProcessStartInfo();
vr.FileName = PATH_VOICEROID;
vr.CreateNoWindow = true;
vr.UseShellExecute = false;

//VOICEROIDの起動
Process.Start(vr);
//起動の待ち時間: 5000ms = 5s
Thread.Sleep(5000);
vrp = Process.GetProcessesByName(pname)[0];
var w = new WindowsAppFriend(vrp);
var main = WindowControl.FromZTop(w);
//テキストボックス
tb = main.IdentifyFromZIndex(2, 0, 0, 1, 0, 1, 1);
//再生ボタン
playbtn = new FormsButton(main.IdentifyFromZIndex(2, 0, 0, 1, 0, 1, 0, 3));
//保存ボタン
savebtn = new FormsButton(main.IdentifyFromZIndex(2, 0, 0, 1, 0, 1, 0, 1));
```

今回は起動後”VOICEROID”にマッチするプロセスの1番目だけを取得しそこからコンポーネントを取得したが
SeikaCenterの方ではすべてVOICEROID＋の名前("VOICEROID＋ 東北きりたん EX" 等)を配列に格納し
すべてのプロセスを取得してからVOICEROIDの名前に合うプロセスを取得してたはず。

その後VOICEROIDに喋らせるには

```cs
//テキストボックスにテキストを渡す
tb["Text"](text);
Thread.Sleep(100);
//再生
playbtn.EmulateClick();
//音声読み終わりまで待つ
while (!savebtn.Enabled)
{
    Thread.Sleep(100);
}
```

テキストボックスにテキストを渡した後すぐに再生すると上手く読み上げてくれないのでSleepを入れる必要がある。
また、VOICEROIDは再生中は保存ボタンが押せないので、それを利用して再生終了を検知する。

##Twitterのタイムラインを読み上げるには
フォームやGUI上にタイムラインを表示しない場合はツイート処理のタイミングで読み上げる処理を入れてあげるだけでいい。
一方、タイムラインを表示する場合この方法だと
ツイートの取得→読み上げ→ツイートの取得→読み上げ→...
のように1つのツイートごとの同期処理になり、タイムライン表示と読み上げを別々にしたい
つまりTLは読み込むタイミングで表示していき、読み上げは1ツイートずつ順番に読み上げてもらいたい場合は非同期処理が必要になる。
その場合、キュー(リスト)とタスクを使用すればいい

まずはTaskを定義する

```cs
talktask = new Task(() =>
{
    while (true)
    {
        if (talklist.Count != 0)
        {
            //テキストを入手
            var text = talklist[0];
            tb["Text"](text);
            Thread.Sleep(100);
            //再生
            playbtn.EmulateClick();
            //音声読み終わりまで待つ
            while (!savebtn.Enabled)
            {
                Thread.Sleep(100);
            }
            talklist.RemoveAt(0);
        }

        Thread.Sleep(100);
    }
});
```
終了したTaskや同じTaskは再起動できないので無限ループさせ、キューに要素がある間だけ読み上げてもらう。
無間ループの間にSleepを入れないとCPU使用率が上がるので注意。

次に、ツイートの取得時にキューに追加する処理を加えてあげる

```cs
Action<StatusMessage> printStatus = (message) =>
{
    var status = (message as StatusMessage).Status;
    /* 取得したツイートの処理 */

    /* ここでGUI上に表示するなどする */

    /* ツイートを読み上げてもらいたい形に加工する */

    /* 加工したツイート(talktext)をリスト(talklist)に追加 */
    talklist.Add(talktext);
}
```

あとはVOICEROIDの起動時に一回だけtalktask.Start()をすればいい。

##タイムラインを表示する
今回はアイコン・ScreenName・Tweetの内容を表示する。
###Windows Formsの場合
DataGridViewにタイムラインを表示するようにする。
まずはColumnの追加と設定

```cs
//TimelineとしてDataGridViewを作成
//columnの作成
var columnImage = new DataGridViewImageColumn();
columnImage.Name = "アイコン";
columnImage.Width = 50;
columnImage.ValuesAreIcons = false;
columnImage.SortMode = DataGridViewColumnSortMode.NotSortable;
var columnName = new DataGridViewTextBoxColumn();
columnName.Name = "ユーザー名";
columnName.Width = 100;
columnName.SortMode = DataGridViewColumnSortMode.NotSortable;
var columnText = new DataGridViewTextBoxColumn();
columnText.Name = "ツイート";
columnText.Width = 350;
columnText.SortMode = DataGridViewColumnSortMode.NotSortable;
//columnの追加
Timeline.Columns.Add(columnImage);
Timeline.Columns.Add(columnName);
Timeline.Columns.Add(columnText);
//セルの内容に合わせて、行の高さが自動的に調節されるようにする
Timeline.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.AllCells;
//ツイート列のセルのテキストを折り返して表示する
Timeline.Columns["ツイート"].DefaultCellStyle.WrapMode = DataGridViewTriState.True;
//左側の項目を消す
Timeline.RowHeadersVisible = false;
```

次に行を追加する処理
DataGridViewImageColumnはImage型しか表示できないのでURLから読み込んで加工する必要がある。
[DataGridViewにURLで指定した画像を表示したい](https://okwave.jp/qa/q2721926.html)

```cs
//img = アイコンのURL
//user = ScreenName
//text = ツイートの内容
WebClient wc = new WebClient();
Stream stream = wc.OpenRead(img);
var image = System.Drawing.Image.FromStream(stream);
stream.Close();
Timeline.Rows.Add(image, user, text);
```

後はこれをツイートの読み込みタイミングで実行すればいい

###WPF(Windows Presentation Foundation)の場合
[WPF DataGridの列に画像を表示](https://qiita.com/Tachibana446/items/8aa90d53bb09ead4c5a9)
まずはDataGridに表示する3要素をまとめたクラスを作る

```Tweet.cs
namespace Appname
{
    class Tweet
    {   
        public string ScreenName { get; set; }
        public string Text { get; set; }
        public string IconURL { get; set; }
        
        public Tweet(string sn, string text, string url)
        {
            ScreenName = sn;
            Text = text;
            IconURL = url;
        }
    }
}

```

カラムを作りBindingする

```MainWindow.xaml
<DataGrid x:Name="Timeline" HorizontalAlignment="Left" Height="407" Margin="10,45,0,0" VerticalAlignment="Top"
 Width="474" AutoGenerateColumns="False" IsReadOnly="True" HeadersVisibility="Column" >
    <DataGrid.Columns>
        <DataGridTemplateColumn Width="50" Header="アイコン" CanUserSort="False" CanUserResize="False" CanUserReorder="False">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate DataType="{x:Type local:Tweet}">
                    <Image Source="{Binding IconURL}" />
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
        <DataGridTextColumn Width="100" Header="ScreenName"  Binding="{Binding ScreenName}" CanUserSort="False" CanUserResize="False" CanUserReorder="False">
            <DataGridTextColumn.ElementStyle>
                <Style TargetType="TextBlock">
                    <!-- 折り返しと中央揃えに設定 -->
                    <Setter Property="TextWrapping" Value="Wrap" />
                    <Setter Property="HorizontalAlignment" Value="Center"/>
                    <Setter Property="VerticalAlignment" Value="Center"/>
                </Style>
            </DataGridTextColumn.ElementStyle>
        </DataGridTextColumn>
        <DataGridTextColumn Width="*" Header="Tweet" Binding="{Binding Text}" CanUserSort="False" CanUserResize="False" CanUserReorder="False">
            <DataGridTextColumn.ElementStyle>
                <Style TargetType="TextBlock">
                    <!-- 折り返し,横に引き伸ばし、縦は中央揃えに設定 -->
                    <Setter Property="TextWrapping" Value="Wrap" />
                    <Setter Property="HorizontalAlignment" Value="Stretch"/>
                    <Setter Property="VerticalAlignment" Value="Center"/>
                </Style>
            </DataGridTextColumn.ElementStyle>
        </DataGridTextColumn>
    </DataGrid.Columns>
</DataGrid>

```
Bindingで指定した変数名を表示してくれる。
ImageのSourceにURLを指定してあげれば表示してくれる模様

最後にデータを追加する処理

```MainWindow.xaml.cs
var t = new Tweet(status.User.ScreenName, status.Text, status.User.ProfileImageUrlHttps);
Timeline.Items.Add(t);
```

あとはそれぞれ見栄えを整えたり、Streamingと読み上げのON/OFFをボタンで切り替えるようにしたりすれば完成

###完成
<img src="https://qiita-image-store.s3.amazonaws.com/0/261299/efbff7f4-ec54-f36e-9bce-b6163daa34aa.jpeg" width=300><img src="https://qiita-image-store.s3.amazonaws.com/0/261299/8959fa68-23b8-8ed2-b1b9-580030d6ddc2.jpeg" width=300>
(WPFはElysiumを使用)

#最後に
メインのVOICEROID読み上げのついでにGUIの方の説明もしてしまいましたが正直TweetDeck見ながら読み上げをしてもらえばいいので必要ないと思います。
多分メニューバーつけてそこからVOICEROIDのパスの設定やTwitterの認証、あと現状でミュートワードをテキストファイルにまとめているものをDBに入れたりすることを出来るようにすればもっとまともなクライアントになるんじゃないかと思う。~~(まぁめんどくさいのでやらないだろうけど)~~
初めてのC#と.NETでのGUIアプリケーション作成だったけどなかなか面白かったです。