アノテーションツールELANのデータをpythonからテキストデータにエクスポート


この記事は[こちらの記事](https://qiita.com/fmfm_mdk/items/26ac2faee84b308d5d6a)を読んでくれた人向けです．

# ELANのアノテーションデータを分析したいな！
という時は日常的にありますよね．

何はともあれ，ELANのデータからcsvでもtsvでも，何らかのテキストデータへエクスポートしたいところです．そのためにはELANのGUI上からメニューをポチポチすればよいのですが，手元にたくさんのELANのファイル（`*.eaf`）がある場合はいささか面倒です．

当エントリでは複数のeafファイルを一括でテキストデータにエクスポートできるスクリプトを組んだ結果を報告します．

# 環境など

eafファイルは所詮xmlなので自前でパースすればよいのですが[^1]，それも面倒なのでよさそうなパッケージを探すことにしました．

そこでよさげパッケージである`pympi`を使用することにします．詳細は以下URLを参照．

https://github.com/dopefishh/pympi

以上より，環境として`python3`，パッケージとして`pympi`，また`pandas`も使用することにします．

[^1]: 筆者はあほなので昔そうしましたが

また以下の`test.eaf`を入力とすることにしました．これは3つの注釈層（`tier1`, `tier2`, `tier3`）が定義されたファイルで，ELANの基本的な注釈付け機能しか使用していないような簡単なものです．展開すれば詳細が確認できます．

<details><summary>`test.eaf`</summary><div>

```xml:test.eaf
<?xml version="1.0" encoding="UTF-8"?>
<ANNOTATION_DOCUMENT AUTHOR="" DATE="2021-04-12T18:18:38+09:00" FORMAT="3.0" VERSION="3.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.mpi.nl/tools/elan/EAFv3.0.xsd">
    <HEADER MEDIA_FILE="" TIME_UNITS="milliseconds">
        <MEDIA_DESCRIPTOR MEDIA_URL="file:///D:/Dropbox/Qiita/ELAN/elan-example1.wav" MIME_TYPE="audio/x-wav" RELATIVE_MEDIA_URL="../ELAN/elan-example1.wav"/>
        <PROPERTY NAME="URN">urn:nl-mpi-tools-elan-eaf:bc21cfb1-4991-4fce-9374-e6e106fb9b3e</PROPERTY>
        <PROPERTY NAME="lastUsedAnnotationId">6</PROPERTY>
    </HEADER>
    <TIME_ORDER>
        <TIME_SLOT TIME_SLOT_ID="ts1" TIME_VALUE="1360"/>
        <TIME_SLOT TIME_SLOT_ID="ts2" TIME_VALUE="2550"/>
        <TIME_SLOT TIME_SLOT_ID="ts3" TIME_VALUE="4600"/>
        <TIME_SLOT TIME_SLOT_ID="ts4" TIME_VALUE="5340"/>
        <TIME_SLOT TIME_SLOT_ID="ts5" TIME_VALUE="9360"/>
        <TIME_SLOT TIME_SLOT_ID="ts6" TIME_VALUE="10340"/>
        <TIME_SLOT TIME_SLOT_ID="ts7" TIME_VALUE="17615"/>
        <TIME_SLOT TIME_SLOT_ID="ts8" TIME_VALUE="18607"/>
        <TIME_SLOT TIME_SLOT_ID="ts9" TIME_VALUE="19037"/>
        <TIME_SLOT TIME_SLOT_ID="ts10" TIME_VALUE="19546"/>
        <TIME_SLOT TIME_SLOT_ID="ts11" TIME_VALUE="19670"/>
        <TIME_SLOT TIME_SLOT_ID="ts12" TIME_VALUE="20779"/>
    </TIME_ORDER>
    <TIER LINGUISTIC_TYPE_REF="default-lt" TIER_ID="tier1">
        <ANNOTATION>
            <ALIGNABLE_ANNOTATION ANNOTATION_ID="a1" TIME_SLOT_REF1="ts7" TIME_SLOT_REF2="ts8">
                <ANNOTATION_VALUE>ハンバーグ</ANNOTATION_VALUE>
            </ALIGNABLE_ANNOTATION>
        </ANNOTATION>
        <ANNOTATION>
            <ALIGNABLE_ANNOTATION ANNOTATION_ID="a2" TIME_SLOT_REF1="ts9" TIME_SLOT_REF2="ts10">
                <ANNOTATION_VALUE>食べたい</ANNOTATION_VALUE>
            </ALIGNABLE_ANNOTATION>
        </ANNOTATION>
        <ANNOTATION>
            <ALIGNABLE_ANNOTATION ANNOTATION_ID="a3" TIME_SLOT_REF1="ts11" TIME_SLOT_REF2="ts12">
                <ANNOTATION_VALUE>ひき肉買わなきゃ</ANNOTATION_VALUE>
            </ALIGNABLE_ANNOTATION>
        </ANNOTATION>
    </TIER>
    <TIER LINGUISTIC_TYPE_REF="default-lt" TIER_ID="tier2">
        <ANNOTATION>
            <ALIGNABLE_ANNOTATION ANNOTATION_ID="a4" TIME_SLOT_REF1="ts1" TIME_SLOT_REF2="ts2">
                <ANNOTATION_VALUE>長ネギ</ANNOTATION_VALUE>
            </ALIGNABLE_ANNOTATION>
        </ANNOTATION>
        <ANNOTATION>
            <ALIGNABLE_ANNOTATION ANNOTATION_ID="a5" TIME_SLOT_REF1="ts3" TIME_SLOT_REF2="ts4">
                <ANNOTATION_VALUE>ネギ</ANNOTATION_VALUE>
            </ALIGNABLE_ANNOTATION>
        </ANNOTATION>
    </TIER>
    <TIER LINGUISTIC_TYPE_REF="default-lt" TIER_ID="tier3">
        <ANNOTATION>
            <ALIGNABLE_ANNOTATION ANNOTATION_ID="a6" TIME_SLOT_REF1="ts5" TIME_SLOT_REF2="ts6">
                <ANNOTATION_VALUE>すし酢やん</ANNOTATION_VALUE>
            </ALIGNABLE_ANNOTATION>
        </ANNOTATION>
    </TIER>
    <LINGUISTIC_TYPE GRAPHIC_REFERENCES="false" LINGUISTIC_TYPE_ID="default-lt" TIME_ALIGNABLE="true"/>
    <LANGUAGE LANG_DEF="http://cdb.iso.org/lg/CDB-00130975-001" LANG_ID="und" LANG_LABEL="undetermined (und)"/>
    <CONSTRAINT DESCRIPTION="Time subdivision of parent annotation's time interval, no time gaps allowed within this interval" STEREOTYPE="Time_Subdivision"/>
    <CONSTRAINT DESCRIPTION="Symbolic subdivision of a parent annotation. Annotations refering to the same parent are ordered" STEREOTYPE="Symbolic_Subdivision"/>
    <CONSTRAINT DESCRIPTION="1-1 association with a parent annotation" STEREOTYPE="Symbolic_Association"/>
    <CONSTRAINT DESCRIPTION="Time alignable annotations within the parent annotation's time interval, gaps are allowed" STEREOTYPE="Included_In"/>
</ANNOTATION_DOCUMENT>

```
</div></details>




# 実装

かなり簡単にできます．`pympi`は優秀です．

```python:parse_eaf.py
import pympi.Elan
import pandas as pd

def eaf_to_df( eaf: pympi.Elan.Eaf ) -> pd.DataFrame:
    tier_names = list( eaf.tiers.keys() )

    def timeslotid_to_time( timeslotid: str ) -> float:
        return eaf.timeslots[ timeslotid ] / 1000

    def parse( tier_name: str, tier: dict ) -> pd.DataFrame:
        values = [ (key,) + value[:-1] for key, value in tier.items() ]
        df = pd.DataFrame( values, columns=[ "id", "start", "end", "transcription"] )

        df["start"] = df["start"].apply( timeslotid_to_time )
        df["end"] = df["end"].apply( timeslotid_to_time )
        df["ID"] = df.apply( lambda x: f"{tier_name}-{x.name}", axis=1 )
        df = df.reindex( columns=["ID", "start", "end", "transcription"] )
        
        return df

    dfs = [ parse(tier_name=name, tier=eaf.tiers[name][0]) for name in tier_names ]
    df = pd.concat( dfs )
    df = df.sort_values( "start" )
    df = df.reset_index( drop=True )
    return df


if __name__ == '__main__':
    src = r"test.eaf"

    eaf = pympi.Elan.Eaf( src )
    df = eaf_to_df( eaf )
    print( df )

    df.to_csv( r"test.txt", sep="\t", index=False )

```

ちなみに，出力されるファイルは以下のようなものです．個人的な趣味として，注釈それぞれに一意のIDを割り当てています．

```text:test.txt
ID	start	end	transcription
tier2-0	1.36	2.55	長ネギ
tier2-1	4.6	5.34	ネギ
tier3-0	9.36	10.34	すし酢やん
tier1-0	17.615	18.607	ハンバーグ
tier1-1	19.037	19.546	食べたい
tier1-2	19.67	20.779	ひき肉買わなきゃ
```

# 終わりに

以上より，eafファイルをテキストデータにエクスポートできるスクリプトを紹介しました．複数の入力ファイルを一括で処理したい場合は，適当にfor文でも書いてください．

またeafファイルのバージョンが3.0だと，`pympi`が文字列`Parsing unknown version of ELAN spec... This could result in errors...`をコンソールに出力しました．基本的な機能だけが使用されたeafファイルなら問題なくパースはできるようですが，最新の機能が使用されたeafファイルでどのような挙動を示すかはわかりません．