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


この記事はこちらの記事を読んでくれた人向けです.

ELANのアノテーションデータを分析したいな!

という時は日常的にありますよね.

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

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

環境など

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

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

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

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

test.eaf
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>

実装

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

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を割り当てています.

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ファイルでどのような挙動を示すかはわかりません.


  1. 筆者はあほなので昔そうしましたが