Fountain の台本をパースしてデータ化する

# 前置き

[Fountain](https://fountain.io/) は台本を書くことに特化した記法で、Markdown のような良さがあります。
Fountain で書かれた台本をパースしてデータ化する仕組みを、開発中の Web アプリに追加したので、その紹介をします。

| |
|-|
|この記事を書いたあとに [playscript](https://satamame.github.io/playscript/) というパッケージを作って、fountain (日本式) を採用しました。|

### 作っているもの

演劇の稽古プランを補助するツールを作っています。
配役、出番、稽古の出欠といったデータから、いつどのシーンが稽古できるか、可視化するものです。
-> [GitHub のリポジトリ](https://github.com/satamame/pscweb2) (Python 3.8, Django 3.0.2)

### Fountain を日本語で書く

Fountain を採用する上での問題は、英語の台本を書くことが前提となっていることです。
日本語で書く場合については、以下で考察しています。

- [Fountain for Japanese](https://satamame.github.io/pscn/fountain/for_japanese.html)

# 記法

日本語で書く場合の詳しい考察については上記のリンクを御覧ください。
ここでは私が採用することにした記法について、各要素の例を紹介します。
この記法で書いたサンプルが [こちら](https://github.com/satamame/pscn/blob/master/docs/fountain/example.fountain) にあります (日本語のサンプルがないので自分の作品で恐縮です)。

### 題名と著者名

```
Title: アンダーコントロール
Author: 沙汰青豆
```

### 登場人物のブロック

```
# 登場人物

捨村: 保存容器会社の社員
京野: 保存容器会社の社員
荻島: 捨村たちの先輩
```

### 柱

柱は階層化できます。

```
# 第一幕
## シーン1
### 研究室
```

### セリフ

空行に続けて、人物名の行とセリフの行を書きます。

```
@京野
置く場所ねえぞこれ。
```

### ト書き

パターン定義されていない書き方の行は、ト書きになります。

```
舞台中央やや奥めにデスク。左右から座れるようになっている。
```

# 台本から作るデータ

このツールのために台本から作れるようにしたいデータは、以下の3つです。

- 登場人物のリスト
- シーンのリスト
- 各シーンでの、各登場人物の出番 (セリフ数)

### セリフ数を記録する理由

一般的な香盤表では、その人物が出ているかどうかを「○」印などで表します。
私のツールでは、役者の出欠状況から「どのシーンを稽古すべきか」を判断できるように出番に重みをつけていて、その指標としてセリフ数を記録しています。

# パース

### パース用モジュール

[Fountain のパース用のモジュール](https://github.com/Tagirijus/fountain) を公開している方がいたので、これを使わせてもらうことにしました。
このモジュールを使って Fountain 記法の文字列をパースすると、`Fountain` クラスのオブジェクトになります。
パースするには、コンストラクタに文字列を渡します。

```python
# text は Fountain 形式の文字列
f = fountain.Fountain(string=text)
```

### Fountain オブジェクト

`Fountain` オブジェクトには、以下の属性があります。
この記事のタスクでは、`elements` 属性の内容からデータを作ります。

- **metadata**
    - [Title Page](https://fountain.io/syntax#section-titlepage) にある `key: value` 形式のデータをパースしたもの。
    - 題名や著者名が (元の文書にあれば) 含まれます。
- **elements**
    - `FountainElement` オブジェクトのリスト。
    - `FountainElement` は、柱、セリフ、ト書きといった要素を表します (下記参照)。
- **contents**
    - パースする前の文字列。

### FountainElement オブジェクト

`FountainElement` オブジェクトは、元の Fountain 形式の台本の、行に対応します (連続する複数行に対応する場合もあります)。
この記事のタスクでは、`FountainElement` オブジェクトの属性のうち、以下の属性を使います。

- **element_type**
    - 柱、セリフ、ト書きといった、要素の種類を表します。
- **element_text**
    - 具体的な内容です (柱ならシーン名、セリフならセリフの内容など)。

### パースの例

```python
from fountain import fountain

text = '''
# シーン１

@京野
置く場所ねえぞこれ。
'''

f = fountain.Fountain(string=text)

for e in f.elements:
    print(e.element_type, e.element_text)
```

#### 結果

```
Section Heading シーン１
Empty Line
Character 京野
Dialogue 置く場所ねえぞこれ。
```

# データを作る

欲しいデータを作るための関数を作って、以下のように呼び出すことにしました。

```python
characters, scenes, appearance = data_from_fountain(text)

print(characters)
print(scenes)
print(appearance)
```

### 欲しい返り値

上記の呼び出しの結果、欲しい返り値は以下のようになります。
`characters` は登場人物名のリスト、`scenes` はシーン名のリストです。
`appearance` は、シーンごとの "出番" をリストにしたものです。
"出番" は人物名をキー、セリフ数を値とした `dict` です。

```python
['京野', '捨村', '荻島', '二人', '容器']
['シーン1', 'シーン２', 'シーン３', 'シーン４', 'シーン５', 'シーン６', 'シーン７']
[{'京野': 26, '捨村': 26}, {'荻島': 14, '捨村': 9, '京野': 9}, {'京野': 24, '捨村': 22}, {'捨村': 10, '荻島': 11, '京野': 3}, {'捨村': 21, '京野': 20, '二人': 1, '容器': 1}, {'京野': 6, '捨村': 5}, {'京野': 10, '捨村': 12, '荻島': 20}]
```

### 関数の中身

さほど難しいコードでもないので、丸ごと載せてしまいます。
ポイントは、登場人物のブロックをカウントしないようにしている所です。
登場人物名は、登場人物ブロックからは取らずに、セリフの人物名から集めます。

また、日本語用のルールとして、[Scene Heading](https://fountain.io/syntax#section-slug) と [Section Heading](https://fountain.io/syntax#section-sections) の両方を「シーンの見出し」として扱っています。

```python
def data_from_fountain(text):
    '''Fountain フォーマットの台本からデータを取得
    
    Parameters
    ----------
    text : str
        台本のテキストデータ
    
    Returns
    -------
    characters : list
        Character 行から取得した登場人物名のリスト
    scenes : list
        Scene Heading 行または Section Heading 行から取得したシーン名のリスト
    appearance : list
        シーンごとの、出番 (dict) のリスト
    '''
    
    # パース
    f = fountain.Fountain(string=text)
    
    characters = []
    scenes = []
    appearance = []
    scn_apprs = {}
    
    for e in f.elements:
        # セリフ主の行
        if e.element_type == 'Character':
            # 少なくとも1個のシーンが検出されていれば
            if scenes:
                # 今のシーンのその人物のセリフ数を出番としてカウント
                char_name = e.element_text
                current_count = scn_apprs.get(char_name, 0)
                scn_apprs[char_name] = current_count + 1
                # 登場人物に登録
                if char_name not in characters:
                    characters.append(char_name)
            continue
        # シーン見出し
        if e.element_type in ('Scene Heading', 'Section Heading'):
            # 少なくとも1個のシーンが検出されていれば
            if scenes:
                # 今のシーンが「登場人物」なら、削除
                if scenes[-1] == '登場人物':
                    scenes.pop()
                else:
                    # ここまでの出番を今のシーンの出番とする
                    appearance.append(scn_apprs)
                # 出番をクリア
                scn_apprs = {}
            # 新しいシーンを追加
            scenes.append(e.element_text)
    
    # 最後のシーンの出番をセット
    if scenes:
        if scenes[-1] == '登場人物':
            scenes.pop()
        else:
            appearance.append(scn_apprs)
    
    return characters, scenes, appearance
```

# Web アプリに組み込む

Web アプリに組み込むところの具体的な説明は割愛します。
理由は、Web アプリについても説明が必要になってしまい、この記事の主題から外れるからです。

興味のある方は [GitHub のリポジトリ](https://github.com/satamame/pscweb2) を御覧ください。
といってもまだドキュメントを整備していませんが…。

という訳で、何をやっているかの簡単な説明だけさせて頂きます。

### やっていること

今までは「公演」>「新規作成」とやっていた公演の作成を、台本の画面からも出来るようにしました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/e84c97d7-c384-7e33-3a78-b0a705bb0251.png" width="400" />
この一番下のリンクを押すと、「台本から公演を作成」という画面になります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/e25023b6-ef6b-48ad-6b83-f65d2939803c.png" width="400" />
「作成」を押すと、公演を作成すると同時に、その公演の登場人物、シーン、出番のデータを追加します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/b948269b-1e33-8781-5f76-c9ca52c772c9.png" width="400" />
登場人物のデータが出来ています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/e92b15ee-29c2-3367-5dd9-c0ff88ec3acd.png" width="400" />
シーンのデータも出来ています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/73a59bbd-1a50-3084-4530-19264152555c.png" width="400" />
出番は、「登場人物の詳細」や「シーンの詳細」から確認・編集できます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/af88add0-0bfe-8021-2dac-517a46be9bc6.png" width="400" />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/c6e54d01-5041-c57f-5315-bbe723a78dd0.png" width="400" />
出番は香盤表で一覧できます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/1d77be44-8c16-d95b-7694-0671b2966027.png" width="400" />

# 課題

- 登場人物ブロックの見分け方をもう少し柔軟にすることで、'# 登場人物' 以外の書き方も出来るようにしたいです。
- 登場人物ブロック内の情報を活かして、キャスト順の定義 (現状は登場順) や、正式名とセリフ用の短縮名の対応付けをしたいです。
- 出はけの情報を書き込んで、セリフのない人物の出番を把握できるようにしたいです。
- 他に良い記法がなければ、もっと Fountain を使えるようにしたいので、日本語用のプレビューアやレンダラーを作れたら良いなぁと思っています。
    - 記法についての考察は [こちら](https://satamame.github.io/pscn/) でまとめています。