テレビ放送から諸々扱えそうなテキストを取得する

#はじめに

マイニングに使われていたらしい「ビデオ出力のないビデオカード（RX470）」と「PCIExpress x1が10個以上あるマザーボード（CPU込）」を入手しました。「目的のためのお買い物」ではなく「目的は買ってから考える」のはいつものこと^^;、ということで。

今更マイニングもないし、機械学習とかかな？チャットボット作ってみたいな～、と調べてみると、大量の学習データが必要とか。学習データ、どうやって調達するのかしら？と先人の業を見るに、ネットにある会話コーパスを利用したり、twitterでreplyを集めたり、とからしく。

他に会話を集める方法はないのかしら？と考えていたら
「放送電波に垂れ流しになってる会話を利用すればいいんじゃね？」
となにかが降りてきました。
そういえば、データ放送に文字情報ってあるよね？

#目的

というわけで。テレビの放送電波に乗ってる文字情報を、いろいろに喰わせられるテキストにしてみたいと思います。
実際になにをやるか、というと、地上波放送を録画したTSファイル、たとえば2019/01/07 23:50 から NHK総合 で放送された 「みんなで筋肉体操 腕立て伏せ(２)」 の録画ファイルから
>
皆さん 筋トレしてますか？
｢みんなで筋肉体操｣です｡
筋トレは 継続して行わなければ効果は上がりません｡
楽しんで 筋肉を追い込んでいきましょう｡
今日は 腕立て伏せです｡
分厚い胸板力強い上半身を作りましょう｡
１種目目は 60秒インターミッテント･プッシュアップです｡

……といったテキストファイルを得る、というのを目的とします。
楽しんで「筋肉は裏切らない」を学習させていきましょう（ぇー

#環境
- OS: Ubuntu18.04
- 地上デジタルTVチューナー: PX-S1UD V2.0 
- 録画ソフト: recdvb

今回は文字情報が目的になります。画質などは重要ではないですし、ワンセグで充分ですのでB-CASカードやカードリーダーは不要になります。つか、B-CASカードだのカードリーダーを用意するのがめんどい^^;。
ワンセグにしたのは画質が必要ない、と同時に、フルセグを録画したファイルとかサイズがバカでかいのでね……厳しいのですよ、HDDの容量的に。
フルセグチューナーをワンセグチューナーとして利用するのもなんですが、PX-S1UD が家で利用場所もなく転がっていましたので。秋葉原あたりで500円くらいで売ってるワンセグチューナーはLinuxで使うにはちょっとキツいしね、というのもあります。

ここではすでに PX-S1UD を使って recdvb で録画できる状態にある、ということで話を進めさせていただきます。

#字幕抽出
字幕抽出、の前に。実験用に放送を実際に録画してファイルにしておきましょう。

```
recdvb --sid 1seg 27 120 test.m2ts
```

問題はこの録画したファイルに字幕テキストが入っているかどうか？です。VLCなどで再生できる環境があるのでしたら、字幕を表示することができますので見ておいてください。
NHKの番組であれば、日付変わってから朝7時くらいまでの放送以外はほぼ入っているようです。
NHKと契約していないなどで観れない場合、NHK以外の在京キー局の放送では、ドラマなどはほぼ入っていましたが、生放送では入ってない番組もあるようです。
私が確認した範囲では、日曜日の午前8時30分からのテレビ朝日の番組では字幕が入っていることを確認しています（んー？。

##字幕抽出ソフト選択
字幕を抽出する方法ですが、今回以下の方法を試してみました。

1. Python3 で ariblib を利用して出力
2. Windows用のCaption2AssC.exe を wine を使って利用して出力
3. assdumperを利用して出力

まず 1.ですが、今回録画したワンセグファイルを入れた場合には、なにも出力されませんでした。ちなみに、家で録画PC（Windows10）で録画したフルセグ動画もなにも出力されず、でした（ちゃんと試していないのでおそらくですが、recdvb でフルセグ録画した場合は出力されると思います）。
次に 2.については、録画WinPCによるフルセグ動画からは字幕情報が取れましたが、今回録画したワンセグファイルからは出力できず。
最後に、3.は今回録画したワンセグファイルから出力できました。

なので、今回の用途では 3.を使います。
今回使用する 3. のassdumper（あるいは2.のCaption2AssC）では、Advanced SubStation Alpha（以下 ASS）という字幕の形式になります。これを後に通常のテキストにしていくことにします。

##assdumperビルド
ソースを持ってきます

```
$ svn export https://github.com/eagletmt/eagletmt-recutils/trunk/assdumper
```
assdumper をそのままビルドしたのでは、話者を識別するために字幕でよく使われる文字色が出力されません。なので出力を追加します。
今回は「色が変わったことだけわかればいい」という考えなので、これをそのまま字幕として別のソフトなどに喰わせてもいいものかは知りませんので注意。

```diff
--- assdumper/assdumper.cc      2017-01-19 21:34:56.000000000 +0900
+++ assdumper_/assdumper.cc     2019-02-02 22:18:58.967834742 +0900
@@ -204,6 +204,8 @@ public:
   {
     const unsigned char *end = str + len;
     std::string ans;
+    unsigned char prev_color = 7;
+    std::string colors[] = {"000000","0000ff","00ff00","00ffff","ff0000","ff00ff","ffff00","ffffff"};
     for (const unsigned char *p = str; p < end; ++p) {
       if (0xa0 < *p && *p < 0xff) {
         char eucjp[3];
@@ -233,7 +235,11 @@ public:
         }
         ++p;
       } else if (0x80 <= *p && *p <= 0x87) {
-        // color code. ignore
+        if( prev_color == (*p - 0x80)){
+        }else{
+          ans += "{\\c&H" + colors[*p - 0x80] + "&}";
+        }
+        prev_color = *p - 0x80;
       } else if (*p == 0x0d) {
         // CR -> LF
         ans += "\\n";
```
パッチを当てたら、`make` して出来た assdumper をパスが通った場所にでも置いてください。

##字幕抽出結果
2019/01/27 08:30 からテレビ朝日で放送された「HUGっと！プリキュア」が入った録画ファイルを入れるとこのようになります。

```
$ assdumper rec/20190127/rec24_20190127080818.m2ts
program_number = 1448, program_map_PID = 8136
1 pmt_pids
8136
388 caption pid, PCR_PID = 257
[Script Info]
ScriptType: v4.00+
Collisions: Normal
ScaledBorderAndShadow: yes
Timer: 100.0000

[Events]
　：
（中略）
　：
Dialogue: 0,00:14:18.76,00:14:22.46,Default,,,,,, {\c&H00ffff&}・うぅぅぅ…・\n{\c&Hffffff&}・（さあや）はい 息はいて〜・
Dialogue: 0,00:14:22.46,00:14:25.93,Default,,,,,, {\c&H00ffff&}はぁ〜！ うぅぅ…
Dialogue: 0,00:14:25.93,00:14:31.02,Default,,,,,, {\c&H00ffff&}あぁぁぁ〜…\n{\c&Hffffff&}（さあや）上手 上手
Dialogue: 0,00:14:31.02,00:14:37.04,Default,,,,,, ・（ほまれ）はな！・\n{\c&H00ffff&}うぅっ はぁ…  
Dialogue: 0,00:14:37.04,00:14:40.74,Default,,,,,, （ダイガン）おぉ！\n{\c&H00ffff&}ほまれ…
Dialogue: 0,00:14:40.74,00:14:43.05,Default,,,,,, 間に合ったね
Dialogue: 0,00:14:43.05,00:14:46.52,Default,,,,,, {\c&H00ffff&}来てくれたんだ
Dialogue: 0,00:14:46.52,00:14:50.92,Default,,,,,, はな！ フレフレ！
Dialogue: 0,00:14:50.92,00:14:53.23,Default,,,,,, {\c&H00ffff&}わぁ…
Dialogue: 0,00:14:53.23,00:14:54.85,Default,,,,,, がんばれ！
Dialogue: 0,00:14:54.85,00:14:56.93,Default,,,,,, {\c&H00ffff&}うん
Dialogue: 0,00:14:56.93,00:15:00.40,Default,,,,,, さぁ いくよ 赤ちゃん がんばってる！
Dialogue: 0,00:15:00.40,00:15:06.65,Default,,,,,, {\c&H00ffff&}うん！  
Dialogue: 0,00:15:06.65,00:15:09.89,Default,,,,,, {\c&H00ffff&}＜子どもの頃 なりたかったわたしに
Dialogue: 0,00:15:09.89,00:15:14.05,Default,,,,,, {\c&H00ffff&}わたしは なれたのかな…＞
Dialogue: 0,00:15:14.05,00:15:17.06,Default,,,,,, {\c&H00ffff&}うぅぅ〜 あぁ〜
Dialogue: 0,00:15:17.06,00:15:20.30,Default,,,,,, {\c&H00ffff&}うぅ〜…
Dialogue: 0,00:15:20.30,00:15:23.30,Default,,,,,, {\c&H00ffff&}＜未来は 楽しいことばかりじゃない＞
Dialogue: 0,00:15:23.30,00:15:26.77,Default,,,,,, {\c&H00ffff&}＜めげそうになることも いっぱいある＞
　：
（以下略）
　：
```
このように、ASSの形式で出力されます。
このままでは使い勝手が悪いので、使いやすいようテキスト整形をしていきます。


#テキスト整形
assの字幕ファイルを整形していくわけですが、これが困ったことに「タグ〇〇が入ったら話者が変わる」「××が表れたら一文が終了」などという決まりはないようです。局によって、あるいは番組によってかなりまちまちなようです。あくまで映像・音声とともに流れる字幕ですので、字幕を読んだ人が他の情報と統合して補う、ということなのでしょう。

とはいえ、整形にはある程度ルールを決めていかないと仕方ないので、ルールを決めて整形をしていくことにします。

##整形ルール
とりあえず、今回は以下のような整形ルールで整形をするようにします。

1. 字幕文字列が空の場合は無視する
2. 「音符 + "～"」の場合は音楽が流れているだけとみなし無視する
3. 字幕表示開始時間が、直前の字幕表示終了時刻から5秒以上経過していた場合は、別の文章・会話とする（ブランク行を入れる）
4. 文字色が直前と比較して変化した場合は、別の文章・人の発言とする
5. 「。（読点）」「！」「？」等が字幕行末に来た場合は、次の字幕は別の文章・人の発言とする
6. 字幕行末に「→」等、矢印の記号が来た場合は次の字幕に文章が続くとする
7. 丸括弧の中は、発言している人の名前、または状況説明とみなしてテキストには出力しない
8. カギ括弧（「」）、角括弧（［］）、山括弧（＜＞）などの中は発言や思ってることとしてテキストに出力する

これで **"見ている範囲"** では **"今のところ"** は **"それなりに"** 整形されているようです。
あくまで私が「今のところ」「見ている範囲」ですので頼りすぎるのは危険ですし、後述しますが、すでに駄目な場合もあります。あくまで「それなり」です（責任逃れの防壁準備）。
このルールについてはうまく整形できてない例を見つけ次第、適宜決めていくしかないような気がします。

皆さんにも「テレビを見ているときに迂闊に字幕をONにしてしまい、整形がうまくいきそうにない字幕を見つけて苦しむ」呪いがかかるといいと思います（ぁ？。

##整形用スクリプト

<details><summary>整形のために作成したPythonスクリプトを示します。</summary><div>

```python:ass2text.py
#!/usr/bin/python3
# -*- coding: utf-8 -*-

import sys
import re

noserif = ['♬～', '♬〜', '♪～']
cmark = ['→', '➡']
eos = ['｡', '。', '!', '！', '?', '？', '⁉', '‼']
ndbraket =  ['＜', '＞', '〈', '〉', '《', '》', '≪', '≫', '\[', '\]', '［', '］']
reb = '|'.join(ndbraket) + '|「|」[。｡]{0,1}';
ndbraket.extend(['「', '」', '」。', '」｡'])

def HMSms2as(t):
    ht = re.split('[:.]', t)
    r = ( int(ht[0]) * 60 * 60 * 100 ) + \
        ( int(ht[1]) * 60 * 100) + \
        ( int(ht[2]) * 100 ) + \
        ( int((ht[3]+"0")[:2]) % 100 )
    return r

def ass2array(fn):
    b = False
    r = []

    try:
        f = open(fn)
        l = f.readline()
        while l:
            s = l.replace("\n","")    
            if 0 < len(s):
                if '[' == s[0]:
                    if '[events]' == s[0:8].lower():
                        b = True
                        l = f.readline()
                        continue
                    else:
                        b = False
                else:
                    if b:
                        if 'dialogue:' == s[0:9].lower():
                            c = 'ffffff'
                            d = s[10:].split(',')
                            if 'default' == d[3].lower():
                                txt = [x for x in re.split('({|})', ','.join(d[9:]).strip()) if not ''==x]
                                st = HMSms2as(d[1])
                                et = HMSms2as(d[2])
                                tb = False
                                buf = []
                                for t in txt:
                                    if "{" == t:
                                        tb = True
                                        continue
                                    if "}" == t and tb:
                                        tb = False
                                        continue
                                    if tb:
                                        m = re.search('\\\d{0,1}c&H([0-9a-fA-F]+)', t)
                                        if m:
                                            c = m.group(1)
                                    else:
                                        ta = [x for x in re.split('\\\\[nN]', t.replace('\\h', ' ')) if not ''==x]
                                        for tf in ta:
                                            buf = [tf, c, st, et]
                                            r.append(buf)
            l = f.readline()
        f.close
    except Exception as e:
        print(e)
        pass
    return r


def tsplit(text):
    r = []
    ta = [x.strip() for x in re.split('(\(|\)|（|）|' + reb + ')', text) if not ''==x]
    tb = False
    for t in ta:
        if t in ['(', '（']:
            tb = True
            continue
        if t in [')', '）']:
            tb = False
            continue
        if t in ndbraket:
            continue

        if tb:
            pass
        else:
            if not t in noserif:
                r.append(t)
    return r

if __name__ == '__main__':
    if 2 != len(sys.argv):
        print('Usage: # python %s filename' % sys.argv[0])
        quit()

    aary = ass2array(sys.argv[1])

    j=0
    tary = []
    t = ""
    for i in range(len(aary)):
        if aary[i][0] in noserif:
            continue
        if 0 < i:
            if 500 > ( aary[i][2] - aary[i-1][3]):
                if aary[(i-1)][1] != aary[i][1]:
                    tary.extend(tsplit(t))
                    t = aary[i][0]
                else:
                    if t[-1:] in eos:
                        tary.extend(tsplit(t))
                        t = aary[i][0]
                    elif t[-1:] in cmark:
                        t = t[:-1] + aary[i][0]
                    else:
                        t = t + aary[i][0]
            else:
                tary.extend(tsplit(t))
                tary.append("")
                t = aary[i][0]
        else:
            t = aary[i][0]
    if "" != t:
        tary.extend(tsplit(t))
for t in tary:
    print (t)

```

</div></details>

##整形結果
先に挙げました「HUGっと！プリキュア」のASSファイルをスクリプトを通して整形した結果、以下のようになりました。

```
・うぅぅぅ…・
・
はい 息はいて〜・
はぁ〜！ うぅぅ…あぁぁぁ〜…
上手 上手・
はな！・
うぅっ はぁ…
おぉ！
ほまれ…
間に合ったね
来てくれたんだ
はな！ フレフレ！
わぁ…
がんばれ！
うん
さぁ いくよ 赤ちゃん がんばってる！
うん！
子どもの頃 なりたかったわたしにわたしは なれたのかな…
うぅぅ〜 あぁ〜うぅ〜…
未来は 楽しいことばかりじゃない
めげそうになることも いっぱいある
```

複数人の発言が一行に入っていたのが複数行になっている（「はぁ〜！ うぅぅ…あぁぁぁ〜…」「上手 上手」とか）、1人のセリフが複数行にわたっていたのが1行になっている（「子どもの頃 なりたかったわたしにわたしは なれたのかな」とか）、というのがわかりますでしょうか。

このプリキュアの例ではあまり整形効果なさそうに見えますが（じゃぁなぜ例として挙げた^^;）、最初に挙げた筋肉体操では

```
Dialogue: 0,0:00:31.13,0:00:34.96,Default,,0000,0000,0000,,{\pos(264,438)\c&H00ffff&}筋トレは 継続して行わなければ\N
Dialogue: 0,0:00:31.13,0:00:34.96,Default,,0000,0000,0000,,{\pos(290,518)\c&H00ffff&}効果は上がりません｡\N
Dialogue: 0,0:00:34.96,0:00:38.26,Default,,0000,0000,0000,,{\pos(184,518)\c&H00ffff&}楽しんで 筋肉を追い込んでいきましょう｡\N
Dialogue: 0,0:00:40.77,0:00:43.17,Default,,0000,0000,0000,,{\pos(344,518)\c&H00ffff&}今日は 腕立て伏せです｡\N
Dialogue: 0,0:00:43.17,0:00:46.67,Default,,0000,0000,0000,,{\pos(370,438)\c&H00ffff&}分厚い胸板\N
Dialogue: 0,0:00:43.17,0:00:46.67,Default,,0000,0000,0000,,{\pos(397,518)\c&H00ffff&}力強い上半身を作りましょう｡\N
```
と一文が複数行（複数字幕）にわたることが多いため、整形の効果はわかっていただけるのではないでしょうか？

ただ、先にも書いたとおり「駄目な場合」もありました。先に挙げたプリキュアのASS、別の箇所にある次のような出力を見てみます。

```
Dialogue: 0,15:29:47.81,15:29:52.21,Default,,,,,, {\c&H00ffff&}実はわたし みんなを守るプリキュアなの
Dialogue: 0,15:29:52.21,15:29:54.75,Default,,,,,, {\c&H00ffff&}あっ この子は 空からふってきた
Dialogue: 0,15:29:54.75,15:29:58.45,Default,,,,,, {\c&H00ffff&}不思議な赤ちゃん はぐたん\n{\c&Hffffff&}はぎゅ！
```

整形結果はこうなります。

```
実はわたし みんなを守るプリキュアなのあっ この子は 空からふってきた不思議な赤ちゃん はぐたん
はぎゅ！
```

「なの」と「あっ」の間、同じ人物（同じ文字色）のセリフですが、読点や字幕の時間などでは文章の区切りがわからないため一行になってしまっています。
今回はこれについては妥協します。
こういった場合を機械的に別の文章とできる、なにかいい方法をご存知の方いましたら教えてくださいまし。

#課題・問題
テレビ放送からテキストを取得する、という目的はおおよそ達成できたと思います。会話とかチャットボットの学習に使えるかはともかく（あれ？。

キャスターが淡々と伝えるようなニュース番組では、さすがに会話とはなりませんが、文章素材としてはかなり有用に使えるのではないかと思います。

トーク系のバラエティ番組では会話が取得できる、と思いきや、そうもいかないようです。以下は 2019/01/27 11:25 からテレビ東京で放送された「男子ごはん」のオープニングトーク部分です。

>突然ですが 問題です。
わかりました。
はい 太一さん。
ブーッ！
ブーッ！
腹立つな。
ラストチャンス！
ピンポーン！
やった～！
大正解です。

「問題です」「ブーッ！」「ピンポーン！」などのやりとりの中で問題や解答をちゃんと言っているのですが、字幕テキストに入ってきていません。
画面の映像にはテロップ文字が出てきますから、映像にテロップで文字を入れているから字幕として入れる必要はない、という判断なのでしょう。そのようになってる番組は多く見られます。
このような場合は字幕だけを追っても会話が成立しないことになります。
バラエティ番組などでは、発言や対するツッコミなどがテロップ側に出る番組が多くなっていますので、こういったことが発生しやすいようです。

字幕を会話をデータとして取得したいならば、ドラマやアニメなどを中心に集めた方がいいかもしれません。……若干偏った会話になりそうな気はしますが（ぉ？。

もちろんバラエティ番組全てが会話を取得できないわけではなく、たとえば、司会者が「毎度おなじみ流浪の番組……」と言って始まる番組では、結構綺麗に会話が取得できます。

NHKがほぼ全ての番組で字幕テキストがあるのも印象的でした（2018年の紅白歌合戦の「勝手にシンドバッド」の曲中に「la la la…桑田君！」が入ってきてたくらい）。ただしETVについては、子供向け番組が多いせいでしょうか、漢字がほとんど含まれない番組も多く見られましたので、気に留めておくべきかもしれません。

テキストをどの用途によって使うか、という観点から番組を選択していく必要もあるでしょう。その際は放送波から抽出できる番組情報を使うことを考えていった方がいいかもしれません。

#最後に
本当は、「切りかえしていこう」「なにが切りかえすのよ」「まぁまぁ無かった事にして」「後世まで語り継ぐよ」とか「負けて悔いなし」「さぁさぁ急ぎましょう」「キミの乗った馬車のように」とかの会話を取り出したかったのですが。現在関東で放送されているClassicには字幕情報はないようです。残念。

「ぼくはギャングスターになる」というアニメの「覚悟はいいか？オレはできてる」などのセリフも、と考えていたのですが。こちらも字幕情報がありませんでした。無念。

当初目的に対しての残念無念はありましたが、それなりに面白かったので、よしとしましょう。

「放送に含まれる字幕テキストを字幕以外に使う」というのが調べても、あまり出てこないように思えるのですが、「テキスト情報が常に得られるデバイス」と考えればなかなかに使いでがありそうに思えます。どうでしょう？
そうなるとバラエティ番組などで、映像側テロップに入ってるからと字幕には入ってこないような発言が取得できない、というのは非常に惜しいんですよね。どうにか字幕に入れてほしい、というのは、さすがにわがままですね。

#余談
放送局ごとに別々のチャットボットに学習させていった場合、同じ質問をしたときに個性など出るのでしょうか？
……若干黒い考えも思い浮かびましたが、検証はおまかせします > ここまで読んでくださった面々