日本語諸方言コーパスをDB化して遊ぶ (4) サービスの全体像を決める

連載記事です。第４回は前回お試しで作成した Laravel アプリを **いったん反故にして** 、サービスの全体像を決めて、ルーティングを作るところまで。完全に自分用の作業メモで、説明もいろいろ足りていないと思いますが、ご容赦ください。

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

## 実現したい機能
COJADS は現在、[コーパス検索アプリケーション「中納言」](https://pj.ninjal.ac.jp/corpus_center/corpus-list.html)にてモニター版が利用可能ですので、これとは機能的に差別化したいところです。


### 「談話／話者」ごとの発話総覧
「中納言」版では検索機能をメインに据えており、テキストデータを「談話」や「話者」といったまとまりでは提供していません。また、前述のとおり COJADS の公式サイトで提供されている生データは「談話」を軸に全結合してあり、取り回しが不便です。したがって、これをエンティティごとに分離して、サイト上で閲覧できるようにする、たとえば「談話」だけでなく「話者」ごとに発話を総覧できる機能を提供することには、一定の価値があると考えられます。

### 「発話」の編集
わざわざ SQL を書くことなく、サイト上からデータベースを操作し、発話を編集する機能をつけます。ログもとります[^log]。そこまで必要性は感じられませんが、データベースの勉強がてら実装したいと思います。

[^log]: ログ一覧ページも作ったのですが、解説に真新しいところもないので本連載記事からは割愛しました。

### Excel と TextGrid の相互変換
COJADS では音声・方言テキスト・標準語テキストが発話を単位に紐づけられていますが、公式サイトでは CSV 形式の生データのみが配布されており、音声データや TextGrid 形式（音声分析ソフト Praat 専用形式）のテキストデータは配布されていません。音声と直接紐づいているのは TextGrid ファイルであり、CSV と音声ファイルがあったところでうまく対応を観察することができません。

また TextGrid がいずれ配布されたとしても、CSV データを各自で編集してしまうと対応が崩れてしまい、音声分析が困難になります。そこで Excel ファイルと TextGrid ファイルを相互変換する機能をつけたいと思います。

**既に手元に Excel と TextGrid を相互変換する Python スクリプトがありましたので**、これを最大限に活かすべく、

- ファイルをアップロードして
- サーバー上で Python スクリプトにかけて変換して
- それをダウンロードする

という仕組みにします。

## 使用技術
AWS を利用するほどでもないので、今回は Heroku のフリープランを利用して完全無料で上記の機能を実装します。ウェブフレームワークとしては前述のとおり **PHP Laravel** を使用します。また Heroku は本番環境で SQLite が使えないので[^sqlite]、データベースとしては **Heroku PostgreSQL** のフリープランを利用します。

[^sqlite]: というか本番環境で SQLite を使えるサービスのほうが少なそう。

- [Postgres - SQL データベース・サービス | Heroku](https://jp.heroku.com/postgres)

Heroku は非常に簡便に利用開始できるサービスですが、その分ビルド後に root 権限が使えなかったり、特にフリープランでは制限されている部分も多く、初心者には難しい点がいくつかありましたので、そこらへんは第９回に説明を試みます。

また、せっかくなので **SPA (Single Page Application)** 化します。SPA を実現するフレームワークはいくつかありますが、今回は学習コストの低そうな Vue Router を採用します。下記のチュートリアルが大いに参考になりました。

- [Vue.js + LaravelでシンプルなSPA構築チュートリアル：概要編 | Qiita](https://qiita.com/minato-naka/items/2d2def4d66ec88dc3ca2)

## 画面遷移図
必要となるページを数え上げて、簡単な画面遷移図を作ります。Qiita で表示しても不快にならないよう、いくつかに分けて描いてみます。

エレガントではないですが、アイコニックで分かりやすく作画コストも低いので [draw.io](https://www.draw.io/) を使用しました。

「談話」ごとの発話総覧と編集機能は以下のようにしましょう。「談話」テーブルには非常に多くの情報が含まれている（ファイル記号、データ名、収録年月日、収録場所、収録担当者、編集担当者、話題、談話ジャンル）ため、これをすべて一画面に表示すると邪魔になります。そこで「談話一覧」ページには、データの性質に影響を与えない要素（収録担当者、編集担当者など）は表示しないようにして、「発話詳細」ページでだけ確認できるようにします。また、「発話編集」ページで更新処理を行なったら「発話詳細」ページに遷移させます。

![func_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526487/5715bf98-b201-93ab-0baa-ebf189971b2f.png)

「話者」ごとの発話総覧は、単純に発話をまとめるだけではつまらないので、簡単な文字列処理を噛ませて、発話ごとではなく文ごとに提示するようにしましょう。

![func_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526487/09e77e84-9ada-ca37-c598-d44a09248af1.png)

ファイル変換画面は、とりあえずアップロードして、変換して、ダウンロードして、削除できればよいので、素朴なのを用意しておきます。

![func_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526487/cef126c7-c448-188f-32fb-2a82feac5266.png)


## プロジェクトの準備
では作成していきましょう。まずは Laravel プロジェクトを作っていきます。やることは基本的に上記参考サイトと同じで、適当なフォルダで `composer` にプロジェクトを作ってもらい、必要なパッケージ等を Laravel (`php artisan`) や `npm` の助けを借りて導入していきます。

```cmd
composer create-project laravel/laravel --prefer-dist cojads
composer require laravel/ui
php artisan ui vue
npm install
npm install --save vue-router
```

これからプロジェクトの内実を作っていきますが、ガワ（ルーティングやテンプレート）から作り始めて、内実（コンポーネントやコントローラ）はあとで作りこむ、という手順を貫きます。

## SPA のテンプレート作成
今回は SPA を作るので、ガワ（テンプレート）として唯一のページ `app.blade.php` を作成します。Laravel プロジェクトではウェブサイトとして通常アクセスした際のルーティングが `web.php` で規定されるので、まずここを編集して任意のパスを `app.blade.php` にルーティングします。ただし今回のサービスはあとで `/storage` に直接アクセスする必要が出てくるので、`/storage` 以下にだけは特別なルーティングをかけないようにしておきます。

```php:routes/web.php
<?php
use Illuminate\Support\Facades\Route;
Route::get('/{any}', function() {
    return view('app');
})->where('any', '^(?!storage).*');
```

そうしたら SPA のガワ `app.blade.php` を作ります。Vue Router の処理で `<header-component>` 位置にヘッダーが、`<footer-component>` 位置にフッターが、`<router-view>` 位置に各ページの内容が読み込まれるようになっています。

bootstrap で固定ヘッダーを作る関係で、`<rooter-view>` の上下に適当に余白を設けています（汚い）。

```html:resources/views/app.blade.php
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <base href="/" />
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="robots" content="noindex">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>{{ config('app.name', 'COJADS App') }}</title>
    <link href="{{ mix('/css/app.css') }}" rel="stylesheet">
</head>
<body>

<div id="app">
    <header-component></header-component>
    <div class="px-5 py-5"><div class="py-3">
        <router-view></router-view>
    </div></div>
    <footer-component></footer-component>
</div>

<script src="{{ mix('/js/app.js') }}" defer></script>

</body>
</html>
```

## コンポーネントのルーティング
続いてテンプレートに挿入されるコンポーネントのルーティングを行ないます。コンポーネントの登録とそのルーティングは `app.js` で管理されているので、ここを編集します。以下ではヘッダー・フッター・表紙ページ・談話一覧ページの要素を読み込んで適切なルーティングを行なっています。

```javascript:resources/js/app.js
import VueRouter from "vue-router";

// register components
import HeaderComponent from "./components/HeaderComponent";
import FooterComponent from "./components/FooterComponent";
import HomeComponent from "./components/HomeComponent";
import DiscourseIndexComponent from "./components/DiscourseIndexComponent";

require("./bootstrap");
window.Vue = require("vue");
Vue.use(VueRouter);

// routing
const router = new VueRouter({
    mode: "history",
    routes: [
        {
            // ルートにアクセスすると HomeComponent をロードする
            path: "/",
            name: "home",
            component: HomeComponent
        },
        {
            // /discourse にアクセスすると DiscourseIndexComponent をロードする
            path: "/discourse",
            name: "discourse.index",
            component: DiscourseIndexComponent
        }
    ]
});

// ヘッダー・フッター用のカスタムエレメントを定義して、コンポーネントを読み込む
Vue.component("header-component", HeaderComponent);
Vue.component("footer-component", FooterComponent);

const app = new Vue({
    el: "#app",
    router
});
```

## 各コンポーネントの作成
ルーティングの構想ができたらページの内容を作っていきます。先ほど `app.js` で使用することにした４つのコンポーネント（`.vue`ファイル）を `/resources/js/components` 下に作成します（`discourse` は次回）。

```diff:以下のファイルを作成する
+ resources/js/components/HeaderComponent.vue
+ resources/js/components/FooterComponent.vue
+ resources/js/components/HomeComponent.vue
+ resources/js/components/DiscourseIndexComponent.vue
```

### ヘッダー
bootstrap でページ上部に固定するタイプのヘッダーを作ります。先取りになりますが、ヘッダーには `<router-link>` で各コンポーネントへのリンクを張っておきましょう。とはいえ、まだリンク先を用意していないので、この段階でコンパイルするなら適当にコメントアウトする必要があります。

- [API リファレンス #\<router-link> | Vue Router](https://router.vuejs.org/ja/api/)

```vue:resources/js/components/HeaderComponent.vue
<template>
    <header class="navbar fixed-top navbar-dark bg-dark">
        <span class="navbar-brand mb-0 h1">COJADS APP</span>
        <div>
            <span>
                <router-link v-bind:to="{ name: 'home' }">
                    <button class="btn btn-success btn-sm">Top Page</button>
                </router-link>
                <router-link v-bind:to="{ name: 'discourse.index' }">
                    <button class="btn btn-success btn-sm">Discourse List</button>
                </router-link>
                <router-link v-bind:to="{ name: 'speaker.index' }">
                    <button class="btn btn-success btn-sm">Speaker List</button>
                </router-link>
                <router-link v-bind:to="{ name: 'convert' }">
                    <button class="btn btn-success btn-sm">Toolkits</button>
                </router-link>
            </span>
        </div>
    </header>
</template>

<script>
export default {};
</script>
```

### フッター
フッターも適当に作ります。

```vue:resources/js/components/FooterComponent.vue
<template>
    <footer class="navbar fixed-bottom navbar-dark bg-dark">
        <span class="navbar-brand mb-0 h1">(c) 2020 @a_eau_</span>
    </footer>
</template>

<script>
export default {};
</script>
```


### ホーム画面
特に書くこともないので、最小限だけ作ります。

```vue:resources/js/components/HomeComponent.vue
<template>
    <div>
        Welcome to COJADS App!
    </div>
</template>

<script>
export default {};
</script>
```



## コンパイル・ローカルサーバで確認
以上のコードを書き終えたら、一度コンパイルして確認しましょう。以下のコマンドを実行すると、もろもろの必要なものを取り込んでいい感じにコンパイルしてくれます。

- [アセットのコンパイル(Mix) 7.x Laravel](https://readouble.com/laravel/7.x/ja/mix.html)

```cmd:cmd
npm run dev あるいは npm run production
```


コンパイルが済んだら Laravel の以下コマンドでローカルサーバを立ち上げて、`localhost:8000` にアクセスしてみましょう。404 や 500 エラーが出ずに想定通りのページが表示されれば成功です。

```cmd:cmd
php artisan serve
```

## 次回
Heroku で SQLite が使えないことを知って PostgreSQL に乗り換える回です。

- 第５回： [日本語諸方言コーパスをDB化して遊ぶ (5) データベースの移行とモデルの作成](https://qiita.com/a_eau_/items/3cad488add22bfacfeaa)