日本語諸方言コーパスをDB化して遊ぶ (7) 話者ごとの発話総覧を作る

連載記事です。前回、談話ごとの発話総覧を作ったので、今回は話者ごとの発話総覧を作ります。やることはあまり変わりません。完全に自分用の作業メモで、説明もいろいろ足りていないと思いますが、ご容赦ください。

- 第１回： [日本語諸方言コーパスをDB化して遊ぶ (1) 構成を考える](https://qiita.com/a_eau_/items/dcb83886c4b580565ce7)
- 第２回： [日本語諸方言コーパスをDB化して遊ぶ (2) SQLite3 で DB 化](https://qiita.com/a_eau_/items/0bb4f3f8271c6dbc12a5)
- 第３回： [日本語諸方言コーパスをDB化して遊ぶ (3) PHP Laravel で操作する](https://qiita.com/a_eau_/items/09f648ede77fd22e7585)
- 第４回： [日本語諸方言コーパスをDB化して遊ぶ (4) サービスの全体像を決める](https://qiita.com/a_eau_/items/0af125f960dd1d16fcde)
- 第５回： [日本語諸方言コーパスをDB化して遊ぶ (5) データベースの移行とモデルの作成](https://qiita.com/a_eau_/items/3cad488add22bfacfeaa)
- 第６回： [日本語諸方言コーパスをDB化して遊ぶ (6) 談話ごとの発話総覧を作る](https://qiita.com/a_eau_/items/2a7072e3cab4729623cb)
- 第７回： 日本語諸方言コーパスをDB化して遊ぶ (7) 話者ごとの発話総覧を作る　←今ここ
- 第８回： [日本語諸方言コーパスをDB化して遊ぶ (8) ファイル形式変換機能をつける](https://qiita.com/a_eau_/items/3f196631348fec1cfc8d)
- 第９回： [日本語諸方言コーパスをDB化して遊ぶ (9) Heroku でデプロイする](https://qiita.com/a_eau_/items/1ae966836d0cf4a6ba0e)

## 画面遷移図
画面遷移図を再掲します。前回よりずっとシンプルなつくりですが、発話単位で分割されているテキストを結合して、文単位に切りなおす操作を挟みます。

![func_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526487/d96e2756-6c8b-1284-4432-3834bfea83a0.png)

## コンポーネントのルーティング
画面遷移図のとおりにルーティングします。今回作りこむ２画面を登録しましょう。

```diff:resources/js/app.js
+ import SpeakerIndexComponent from "./components/SpeakerIndexComponent";
+ import SpeakerShowComponent from "./components/SpeakerShowComponent";

+         {
+             path: "/speaker",
+             name: "speaker.index",
+             component: SpeakerIndexComponent,
+             props: true
+         },
+         {
+             path: "/speaker/:speakerid",
+             name: "speaker.show",
+             component: SpeakerShowComponent,
+             props: true
+         },
```

## 各コンポーネントの作成
### 話者一覧
特筆すべき箇所はありません。全話者の情報を取得して（`getSpeakers` 関数）、「話者ＩＤ」「話者生年」「話者性別」を表示するだけです。今回はページ幅に余裕があるので［閲覧］ボタンを別途用意しても大丈夫でしょう。

```vue:resources/js/components/SpeakerIndexComponent.vue
<template>
<table class="table table-sm table-hover">
    <thead class="thead-dark">
            <tr>
                <th>話者 ID</th>
                <th>話者生年</th>
                <th>話者性別</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            <tr v-for="s in speakers" v-bind:key="s.speakerid">
                <td>{{ s.speakerid }}</td>
                <td>{{ s.speakerbirthyear }}</td>
                <td>{{ s.speakersex }}</td>
                <td>
                    <div class="text-right">
                        <router-link 
                            v-bind:to="{ name: 'speaker.show', params: { speakerid: s.speakerid } }"
                        >
                            <button class="btn btn-success btn-sm text-nowrap">
                                閲覧
                            </button>
                        </router-link>
                    </div>
                </td>
            </tr>
        </tbody>
    </table>
</template>

<script>
export default {
    data: function() {
        return {
            speakers: []
        };
    },
    methods: {
        getSpeakers() {
            spinner.style.opacity = 1;
            axios.get("/api/speaker").then(res => {
                this.speakers = res.data;
                spinner.style.opacity = 0;
            });
        }
    },
    mounted() {
        this.getSpeakers();
    }
};
</script>
```


### 話者の発話総覧
話者情報を表示する部分と発話総覧の部分に分かれています。処理もそれぞれに用意します。

前者では、パスで話者ＩＤを渡して（`props` の部分）、データベースから当該話者の情報だけ取ってきて表示します（`getSpeakerInfo` 関数）。

後者では、データベースから発話の全レコードを取得してきて、それをフロントエンドで加工して文単位に切りなおしています（`getUtterances` 関数）。この処理はサーバでやってもいいかもしれません。

```vue:resources/js/components/SpeakerShowComponent.vue
<template>
    <div>
        <table class="table table-sm table-hover">
            <tbody>
                <tr class="table-success">
                    <td nowrap>話者ＩＤ</td>
                    <td>{{ speakerInfo.speakerid }}</td>
                </tr>
                <tr class="table-success">
                    <td nowrap>話者生年</td>
                    <td>{{ speakerInfo.speakerbirthyear }}</td>
                </tr>
                <tr class="table-success">
                    <td nowrap>話者性別</td>
                    <td>{{ speakerInfo.speakersex }}</td>
                </tr>
            </tbody>
        </table>
        <div class="pt-3">
            <table class="table table-sm table-striped table-hover">
                <thead>
                    <tr class="thead-dark">
                        <th><div class="text-center">発話</div></th>
                    </tr>
                </thead>
                <tbody>
                    <tr v-for="sentence in sentences" v-bind:key="sentence.id">
                        <td>
                            <div class="px-4">{{ sentence.d }}。</div>
                            <div class="px-4">{{ sentence.s }}。</div>
                        </td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>
</template>

<script>
export default {
    props: {
        speakerid: String
    },
    data: () => {
        return {
            sentences: [],
            speakerInfo: {}
        };
    },
    methods: {
        getUtterances() {
            axios
                .get("/api/speaker/" + this.speakerid + "/utterances")
                .then(res => {
                    // 取得した発話をいったん全て結合する
                    let dTextStr = "";
                    let sTextStr = "";
                    for (let i = 0, i_len = res.data.length; i < i_len; i++) {
                        // 文節区切りをつぶさないよう適宜スペースを入れる
                        dTextStr += "　" + res.data[i].dialecttext;
                        sTextStr += "　" + res.data[i].standardtext;
                    }
                    // 句点で切って this.sentences に入れていく
                    const dSentences = dTextStr.split("。");
                    const sSentences = sTextStr.split("。");
                    for (let i = 0, i_len = dSentences.length; i < i_len; i++) {
                        // sentences は id, d(ialect), s(tandard) の連想配列にする
                        if (dSentences[i] && sSentences[i]) {
                            this.sentences.push({
                                id: i,
                                d: dSentences[i].trim(),
                                s: sSentences[i].trim()
                            });
                        }
                    }
                });
        },
        getSpeakerInfo() {
            axios.get("/api/speaker/" + this.speakerid).then(res => {
                // correction で返ってくるので [0] で取得する
                this.speakerInfo = {
                    speakerid: res.data[0]["speakerid"],
                    speakerbirthyear: res.data[0]["speakerbirthyear"],
                    speakersex: res.data[0]["speakersex"]
                };
            });
        }
    },
    mounted() {
        this.getUtterances();
        this.getSpeakerInfo();
    }
};
</script>
```

## コントローラへのルーティング
使用するコントローラはひとつ（`SpeakerController`）でよさそうなので、API のルーティングも悩むことはありません。関数の名前も適当に考えます。

```php:routes/api.php
Route::get('/speaker', 'SpeakerController@index');
Route::get('/speaker/{speakerid}', 'SpeakerController@show');
Route::get('/speaker/{speakerid}/utterances', 'SpeakerController@getUtterances');
```

## コントローラの作成
先ほど使うことにした３つの関数を実装していきます。前回と同様に、Controller で `->first()` するのではなく、`->get()` してコレクションを返してフロントエンドで絞る感じにしています。

```php:app/Http/Controllers/SpeakerController.php
<?php
namespace App\Http\Controllers;
use App\Models\Speaker;
use Illuminate\Support\Facades\DB;

class SpeakerController extends Controller{
    // 全レコード取得
    public function index(){
        $md = new Speaker();
        $data = $md->getData();
        return $data;
    }

    // 指定した話者 ID に一致する話者レコードを返す
    public function show(String $speakerid){
        $data = DB::table('speaker')
        ->select('speaker.*')
        ->where('speaker.speakerid', '=', $speakerid)
        ->get();
        return $data;
    }

    // 指定した話者 ID をもつ発話をすべて返す
    public function getAllUtterances(String $speakerid){
        $data = DB::table('utterance')
        ->select('utterance.*')
        ->where('utterance.speakerid', '=', $speakerid)
        ->get();
        return $data;
    }

}
```

## 完成形
うまくいけばこんな具合に表示されるはずです。

`/speaker`
![speakerindex.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526487/d0b709b4-d60e-b54e-a52a-0a328f391dbe.png)

`/speaker/02_e_099::C`
![speakershow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526487/5e78a125-7a03-80bb-df48-ed6d0003cf5a.png)

## 次回
第３の機能「ファイルの変換」を作りこんでいきます。

- 第８回： [日本語諸方言コーパスをDB化して遊ぶ (8) ファイル形式変換機能をつける](https://qiita.com/a_eau_/items/3f196631348fec1cfc8d)