この記事はこちらの記事を読んでくれた人向けです.
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`
<?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
は優秀です.
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を割り当てています.
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ファイルでどのような挙動を示すかはわかりません.
-
筆者はあほなので昔そうしましたが ↩