diff --git a/.mlc-config.json b/.mlc-config.json deleted file mode 100644 index 1d38867..0000000 --- a/.mlc-config.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "_comment": "Markdown Link Checker configuration, see https://github.com/gaurav-nelson/github-action-markdown-link-check and https://github.com/tcort/markdown-link-check", - "ignorePatterns": [ - { - "pattern": "^http://localhost" - }, - { - "pattern": "^https://doi.org/" - }, - { - "pattern": "^https://github.com/.*/settings/secrets/actions$" - }, - { - "pattern": "^https://github.com/organizations/.*/repositories/new" - }, - { - "pattern": "^https://test.pypi.org" - }, - { - "pattern": "^https://bestpractices.coreinfrastructure.org/projects/" - }, - { - "pattern": "^https://readthedocs.org/dashboard/import.*" - } - ], - "replacementPatterns": [ - ], - "retryOn429": true, - "timeout": "20s" -} diff --git a/data/testcorpus/conversations.json b/data/testcorpus/conversations.json new file mode 100644 index 0000000..d038096 --- /dev/null +++ b/data/testcorpus/conversations.json @@ -0,0 +1 @@ +{"/ulwa1/ulwa014": {"meta": {}, "vectors": []}} \ No newline at end of file diff --git a/data/testcorpus/corpus.json b/data/testcorpus/corpus.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/data/testcorpus/corpus.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/data/testcorpus/index.json b/data/testcorpus/index.json new file mode 100644 index 0000000..5aed485 --- /dev/null +++ b/data/testcorpus/index.json @@ -0,0 +1 @@ +{"utterances-index": {}, "speakers-index": {}, "conversations-index": {}, "overall-index": {}, "version": 1, "vectors": []} \ No newline at end of file diff --git a/data/testcorpus/speakers.json b/data/testcorpus/speakers.json new file mode 100644 index 0000000..468ad1e --- /dev/null +++ b/data/testcorpus/speakers.json @@ -0,0 +1 @@ +{"Tang": {"meta": {}, "vectors": []}, "Yan": {"meta": {}, "vectors": []}} \ No newline at end of file diff --git a/data/testcorpus/utterances.jsonl b/data/testcorpus/utterances.jsonl new file mode 100644 index 0000000..52e8599 --- /dev/null +++ b/data/testcorpus/utterances.jsonl @@ -0,0 +1,10 @@ +{"id": "0", "conversation_id": "/ulwa1/ulwa014", "text": "U oughs inim t\u00ef samting yan", "speaker": "Tang", "meta": {}, "reply-to": null, "timestamp": 1332718704, "vectors": []} +{"id": "1", "conversation_id": "/ulwa1/ulwa014", "text": "mbam ndul ma wandam ana", "speaker": "Yan", "meta": {}, "reply-to": null, "timestamp": 1332732704, "vectors": []} +{"id": "2", "conversation_id": "/ulwa1/ulwa014", "text": "M\u00ef inim wandam bai anapa nd", "speaker": "Tang", "meta": {}, "reply-to": null, "timestamp": 1332743704, "vectors": []} +{"id": "3", "conversation_id": "/ulwa1/ulwa014", "text": "lunda we nd\u00efm\u00efne in", "speaker": "Yan", "meta": {}, "reply-to": null, "timestamp": 1332754704, "vectors": []} +{"id": "4", "conversation_id": "/ulwa1/ulwa014", "text": "k\u00efnakape ak\u00efnaka", "speaker": "Tang", "meta": {}, "reply-to": null, "timestamp": 1332765704, "vectors": []} +{"id": "5", "conversation_id": "/ulwa1/ulwa014", "text": "coughs nd\u00efm\u00efne we ndul wa le we nd\u00eft\u00ef ak\u00efnakape malimap mat\u00ef yawa mananda", "speaker": "Yan", "meta": {}, "reply-to": null, "timestamp": 1332776704, "vectors": []} +{"id": "6", "conversation_id": "/ulwa1/ulwa014", "text": "mananda", "speaker": "Tang", "meta": {}, "reply-to": null, "timestamp": 1332787704, "vectors": []} +{"id": "7", "conversation_id": "/ulwa1/ulwa014", "text": "da", "speaker": "Yan", "meta": {}, "reply-to": null, "timestamp": 1332788704, "vectors": []} +{"id": "8", "conversation_id": "/ulwa1/ulwa014", "text": "e k\u00efkal awi ak\u00efnakape", "speaker": "Tang", "meta": {}, "reply-to": null, "timestamp": 1332789704, "vectors": []} +{"id": "9", "conversation_id": "/ulwa1/ulwa014", "text": "at\u00efm inim.", "speaker": "Yan", "meta": {}, "reply-to": null, "timestamp": 1332999704, "vectors": []} diff --git a/data/ulwa json corpus/conversations.json b/data/ulwa json corpus/conversations.json new file mode 100644 index 0000000..d038096 --- /dev/null +++ b/data/ulwa json corpus/conversations.json @@ -0,0 +1 @@ +{"/ulwa1/ulwa014": {"meta": {}, "vectors": []}} \ No newline at end of file diff --git a/data/ulwa json corpus/corpus.json b/data/ulwa json corpus/corpus.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/data/ulwa json corpus/corpus.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/data/ulwa json corpus/index.json b/data/ulwa json corpus/index.json new file mode 100644 index 0000000..5aed485 --- /dev/null +++ b/data/ulwa json corpus/index.json @@ -0,0 +1 @@ +{"utterances-index": {}, "speakers-index": {}, "conversations-index": {}, "overall-index": {}, "version": 1, "vectors": []} \ No newline at end of file diff --git a/data/ulwa json corpus/speakers.json b/data/ulwa json corpus/speakers.json new file mode 100644 index 0000000..468ad1e --- /dev/null +++ b/data/ulwa json corpus/speakers.json @@ -0,0 +1 @@ +{"Tang": {"meta": {}, "vectors": []}, "Yan": {"meta": {}, "vectors": []}} \ No newline at end of file diff --git a/data/ulwa json corpus/utterances.jsonl b/data/ulwa json corpus/utterances.jsonl new file mode 100644 index 0000000..52e8599 --- /dev/null +++ b/data/ulwa json corpus/utterances.jsonl @@ -0,0 +1,10 @@ +{"id": "0", "conversation_id": "/ulwa1/ulwa014", "text": "U oughs inim t\u00ef samting yan", "speaker": "Tang", "meta": {}, "reply-to": null, "timestamp": 1332718704, "vectors": []} +{"id": "1", "conversation_id": "/ulwa1/ulwa014", "text": "mbam ndul ma wandam ana", "speaker": "Yan", "meta": {}, "reply-to": null, "timestamp": 1332732704, "vectors": []} +{"id": "2", "conversation_id": "/ulwa1/ulwa014", "text": "M\u00ef inim wandam bai anapa nd", "speaker": "Tang", "meta": {}, "reply-to": null, "timestamp": 1332743704, "vectors": []} +{"id": "3", "conversation_id": "/ulwa1/ulwa014", "text": "lunda we nd\u00efm\u00efne in", "speaker": "Yan", "meta": {}, "reply-to": null, "timestamp": 1332754704, "vectors": []} +{"id": "4", "conversation_id": "/ulwa1/ulwa014", "text": "k\u00efnakape ak\u00efnaka", "speaker": "Tang", "meta": {}, "reply-to": null, "timestamp": 1332765704, "vectors": []} +{"id": "5", "conversation_id": "/ulwa1/ulwa014", "text": "coughs nd\u00efm\u00efne we ndul wa le we nd\u00eft\u00ef ak\u00efnakape malimap mat\u00ef yawa mananda", "speaker": "Yan", "meta": {}, "reply-to": null, "timestamp": 1332776704, "vectors": []} +{"id": "6", "conversation_id": "/ulwa1/ulwa014", "text": "mananda", "speaker": "Tang", "meta": {}, "reply-to": null, "timestamp": 1332787704, "vectors": []} +{"id": "7", "conversation_id": "/ulwa1/ulwa014", "text": "da", "speaker": "Yan", "meta": {}, "reply-to": null, "timestamp": 1332788704, "vectors": []} +{"id": "8", "conversation_id": "/ulwa1/ulwa014", "text": "e k\u00efkal awi ak\u00efnakape", "speaker": "Tang", "meta": {}, "reply-to": null, "timestamp": 1332789704, "vectors": []} +{"id": "9", "conversation_id": "/ulwa1/ulwa014", "text": "at\u00efm inim.", "speaker": "Yan", "meta": {}, "reply-to": null, "timestamp": 1332999704, "vectors": []} diff --git a/data/ulwa_testdata_convokit_format.csv b/data/ulwa_testdata_convokit_format.csv new file mode 100644 index 0000000..4e8e5c0 --- /dev/null +++ b/data/ulwa_testdata_convokit_format.csv @@ -0,0 +1,11 @@ +timestamp,speaker,text,translation,conversation_id,utterance_raw,reply_to +1332718704,Tang,U oughs inim tï samting yan,"Lorem ipsum dolor sit amet.",/ulwa1/ulwa014,U oughs inim tï samting yangama ul matï akïnakape,None +1332732704,Yan,mbam ndul ma wandam ana,"At neque fugit eum reprehenderit labore et exercitationem voluptatem. eos odio aspernatur.",/ulwa1/ulwa014,wimbam ndul ma wandam anapa ol welunda nïkap tu mananda yangama,None +1332743704,Tang,Mï inim wandam bai anapa nd,"a veritatis tempore sit vitae quaerat sed consequatur amet qui nisi facilis et perferendis nisi ut maiores consequatur.",/ulwa1/ulwa014,Mï inim wandam bai anapa ndïtï ka welunda unan,None +1332754704,Yan,lunda we ndïmïne in,,/ulwa1/ulwa014,ata welunda we ndïmïne ind,None +1332765704,Tang,kïnakape akïnaka,,/ulwa1/ulwa014,i akïnakape akïnakap,None +1332776704,Yan,coughs ndïmïne we ndul wa le we ndïtï akïnakape malimap matï yawa mananda,"Et illo facere vel magni necessitatibus est aspernatur numquam",/ulwa1/ulwa014,[coughs] I inim oughs ka lopop mananda bai kïkal yangama we ini,None +1332787704,Tang,mananda,,/ulwa1/ulwa014,n mananda ndïtï ka akïnakape wimbam,None +1332788704,Yan,da,,/ulwa1/ulwa014,da ndïtï ka,None +1332789704,Tang,e kïkal awi akïnakape,"onsequatur amet qui nisi facilis et perferendis nisi ut",/ulwa1/ulwa014,e kïkal awi akïnakape manï lï,None +1332999704,Yan,atïm inim.,"itae quaerat sed consequatur amet",/ulwa1/ulwa014,atïm inim.,None diff --git a/data/ulwa_testdata_sktalk_format.csv b/data/ulwa_testdata_sktalk_format.csv new file mode 100644 index 0000000..a8116f6 --- /dev/null +++ b/data/ulwa_testdata_sktalk_format.csv @@ -0,0 +1,11 @@ +begin,end,participant,utterance,translation,source,utterance_raw +00:00:00.917,00:00:05.604,Tang,U oughs inim tï samting yan,"Lorem ipsum dolor sit amet.",/ulwa1/ulwa014,U oughs inim tï samting yangama ul matï akïnakape +00:00:04.830,00:00:09.080,Yan,mbam ndul ma wandam ana,"At neque fugit eum reprehenderit labore et exercitationem voluptatem. eos odio aspernatur.",/ulwa1/ulwa014,wimbam ndul ma wandam anapa ol welunda nïkap tu mananda yangama +00:00:06.090,00:00:09.450,Tang,Mï inim wandam bai anapa nd,"a veritatis tempore sit vitae quaerat sed consequatur amet qui nisi facilis et perferendis nisi ut maiores consequatur.",/ulwa1/ulwa014,Mï inim wandam bai anapa ndïtï ka welunda unan +00:00:09.534,00:00:10.333,Yan,lunda we ndïmïne in,,/ulwa1/ulwa014,ata welunda we ndïmïne ind +00:00:10.333,00:00:11.143,Tang,kïnakape akïnaka,,/ulwa1/ulwa014,i akïnakape akïnakap +00:00:11.143,00:00:18.240,Yan,coughs ndïmïne we ndul wa le we ndïtï akïnakape malimap matï yawa mananda,"Et illo facere vel magni necessitatibus est aspernatur numquam",/ulwa1/ulwa014,[coughs] I inim oughs ka lopop mananda bai kïkal yangama we ini +00:00:11.477,00:00:12.205,Tang,mananda,,/ulwa1/ulwa014,n mananda ndïtï ka akïnakape wimbam +00:00:14.390,00:00:15.696,Yan,da,,/ulwa1/ulwa014,da ndïtï ka +00:00:17.972,00:00:20.722,Tang,e kïkal awi akïnakape,"onsequatur amet qui nisi facilis et perferendis nisi ut",/ulwa1/ulwa014,e kïkal awi akïnakape manï lï +00:00:18.240,00:00:21.970,Yan,atïm inim.,"itae quaerat sed consequatur amet",/ulwa1/ulwa014,atïm inim. diff --git a/data/vamale json corpus/conversations.json b/data/vamale json corpus/conversations.json new file mode 100644 index 0000000..317e1b0 --- /dev/null +++ b/data/vamale json corpus/conversations.json @@ -0,0 +1 @@ +{"/vamale1/vamaleE": {"meta": {}, "vectors": []}, "/vamale1/vamaleie": {"meta": {}, "vectors": []}, "/vamale1/vamale-vie": {"meta": {}, "vectors": []}, "/vamale1/vamale-MS": {"meta": {}, "vectors": []}, "/vamale1/vamaleMS": {"meta": {}, "vectors": []}, "/vamale1/vamalen-MS": {"meta": {}, "vectors": []}, "/vamale1/vamale4": {"meta": {}, "vectors": []}} \ No newline at end of file diff --git a/data/vamale json corpus/corpus.json b/data/vamale json corpus/corpus.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/data/vamale json corpus/corpus.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/data/vamale json corpus/index.json b/data/vamale json corpus/index.json new file mode 100644 index 0000000..5aed485 --- /dev/null +++ b/data/vamale json corpus/index.json @@ -0,0 +1 @@ +{"utterances-index": {}, "speakers-index": {}, "conversations-index": {}, "overall-index": {}, "version": 1, "vectors": []} \ No newline at end of file diff --git a/data/vamale json corpus/speakers.json b/data/vamale json corpus/speakers.json new file mode 100644 index 0000000..1fe10f8 --- /dev/null +++ b/data/vamale json corpus/speakers.json @@ -0,0 +1 @@ +{"Ric": {"meta": {}, "vectors": []}, "Rie": {"meta": {}, "vectors": []}} \ No newline at end of file diff --git a/data/vamale json corpus/utterances.jsonl b/data/vamale json corpus/utterances.jsonl new file mode 100644 index 0000000..c93a26e --- /dev/null +++ b/data/vamale json corpus/utterances.jsonl @@ -0,0 +1,10 @@ +{"id": "0", "conversation_id": "/vamale1/vamaleE", "text": "Aoo angovi \u0263ase aoo vinjo thamauke va nyau yanle?", "speaker": "Ric", "meta": {}, "reply-to": null, "timestamp": 1332712704, "vectors": []} +{"id": "1", "conversation_id": "/vamale1/vamaleie", "text": "ta\u0263ua va thamauke \u0272akoe va angovi \u0263ase.", "speaker": "Rie", "meta": {}, "reply-to": null, "timestamp": 1332722704, "vectors": []} +{"id": "2", "conversation_id": "/vamale1/vamale-vie", "text": "Va vinjo ta\u0263ua daa thanga \u0272akoe daa \u0263ase angovi", "speaker": "Ric", "meta": {}, "reply-to": null, "timestamp": 1332732704, "vectors": []} +{"id": "3", "conversation_id": "/vamale1/vamale-MS", "text": "a nyau thapoke va \u0272akoe", "speaker": "Rie", "meta": {}, "reply-to": null, "timestamp": 1332742704, "vectors": []} +{"id": "4", "conversation_id": "/vamale1/vamaleMS", "text": "\u0272akoe va \u0263ananda konaa va \u0263ananda vinjo ", "speaker": "Ric", "meta": {}, "reply-to": null, "timestamp": 1332755704, "vectors": []} +{"id": "5", "conversation_id": "/vamale1/vamaleMS", "text": "o \u0263ase va angovi angovi nea", "speaker": "Rie", "meta": {}, "reply-to": null, "timestamp": 1332766704, "vectors": []} +{"id": "6", "conversation_id": "/vamale1/vamalen-MS", "text": "au nyau aoo thamauke \u0263ase daa ya", "speaker": "Ric", "meta": {}, "reply-to": null, "timestamp": 1332777704, "vectors": []} +{"id": "7", "conversation_id": "/vamale1/vamaleMS", "text": "uke! Daa \u0263ananda thamauke va \u0263ase t", "speaker": "Rie", "meta": {}, "reply-to": null, "timestamp": 1332788804, "vectors": []} +{"id": "8", "conversation_id": "/vamale1/vamale4", "text": "yanle konaa daa thamauke \u0263ananda va", "speaker": "Ric", "meta": {}, "reply-to": null, "timestamp": 1332799904, "vectors": []} +{"id": "9", "conversation_id": "/vamale1/vamale4", "text": "ga nyau va vinjo konaa daa \u0263anand", "speaker": "Rie", "meta": {}, "reply-to": null, "timestamp": 1332792704, "vectors": []} diff --git a/data/vamale_testdata_convokit_format.csv b/data/vamale_testdata_convokit_format.csv new file mode 100644 index 0000000..299f509 --- /dev/null +++ b/data/vamale_testdata_convokit_format.csv @@ -0,0 +1,11 @@ +timestamp,speaker,text,conversation_id,utterance_raw,reply_to +1332712704,Ric,Aoo angovi ɣase aoo vinjo thamauke va nyau yanle?,/vamale1/vamaleE,Aoo angovi ɣase aoo vinjo thamauke va nyau yanle?,None +1332722704,Rie,taɣua va thamauke ɲakoe va angovi ɣase.,/vamale1/vamaleie,ua va thamauke ɲakoe v,None +1332732704,Ric,Va vinjo taɣua daa thanga ɲakoe daa ɣase angovi,/vamale1/vamale-vie,aa thanga ɲakoe daa ɣase,None +1332742704,Rie,a nyau thapoke va ɲakoe,/vamale1/vamale-MS,thapoke va ɲak,None +1332755704,Ric,ɲakoe va ɣananda konaa va ɣananda vinjo ,/vamale1/vamaleMS,ɣnaa va ɣananda vin,None +1332766704,Rie,o ɣase va angovi angovi nea,/vamale1/vamaleMS,va angovi angovi,None +1332777704,Ric,au nyau aoo thamauke ɣase daa ya,/vamale1/vamalen-MS,limyau aoo thamauke ɣase daaa,None +1332788804,Rie,uke! Daa ɣananda thamauke va ɣase t,/vamale1/vamaleMS,ke! Daa ɣananda tham,None +1332799904,Ric,yanle konaa daa thamauke ɣananda va,/vamale1/vamale4,konaa daa,None +1332792704,Rie,ga nyau va vinjo konaa daa ɣanand,/vamale1/vamale4,au va vinjo konaa daa ɣa,None diff --git a/data/vamale_testdata_sktalk_format.csv b/data/vamale_testdata_sktalk_format.csv new file mode 100644 index 0000000..becde13 --- /dev/null +++ b/data/vamale_testdata_sktalk_format.csv @@ -0,0 +1,11 @@ +begin,end,participant,utterance,translation,source,utterance_raw +00:01:33.740,00:01:36.200,Ric,Aoo angovi ɣase aoo vinjo thamauke va nyau yanle?,,/vamale1/vamaleE,Aoo angovi ɣase aoo vinjo thamauke va nyau yanle? +00:02:25.065,00:02:26.445,Rie,taɣua va thamauke ɲakoe va angovi ɣase.,,/vamale1/vamaleie,ua va thamauke ɲakoe v +00:02:26.385,00:02:27.925,Ric,Va vinjo taɣua daa thanga ɲakoe daa ɣase angovi,,/vamale1/vamale-vie,aa thanga ɲakoe daa ɣase +00:03:03.530,00:03:05.130,Rie,a nyau thapoke va ɲakoe,,/vamale1/vamale-MS,thapoke va ɲak +00:03:05.595,00:03:08.355,Ric,ɲakoe va ɣananda konaa va ɣananda vinjo ,,/vamale1/vamaleMS,ɣnaa va ɣananda vin +00:03:08.710,00:03:12.720,Rie,o ɣase va angovi angovi nea,,/vamale1/vamaleMS,va angovi angovi +00:03:12.950,00:03:16.760,Ric,au nyau aoo thamauke ɣase daa ya,,/vamale1/vamalen-MS,limyau aoo thamauke ɣase daaa +00:03:17.070,00:03:19.470,Rie,uke! Daa ɣananda thamauke va ɣase t,,/vamale1/vamaleMS,ke! Daa ɣananda tham +00:15:44.815,00:15:46.395,Ric,yanle konaa daa thamauke ɣananda va,,/vamale1/vamale4,konaa daa +00:26:03.605,00:26:05.345,Rie,ga nyau va vinjo konaa daa ɣanand,,/vamale1/vamale4,au va vinjo konaa daa ɣa diff --git a/notebooks/Make_audio_column.ipynb b/notebooks/Make_audio_column.ipynb new file mode 100644 index 0000000..5371296 --- /dev/null +++ b/notebooks/Make_audio_column.ipynb @@ -0,0 +1,918 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "RUcScYxiyCLa" + }, + "source": [ + "### To do list\n", + "todo:\n", + "- make debug function that logs errors\n", + "- make mel matrix \n", + "- make spec col" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kF0UisGqTpR5" + }, + "source": [ + "### Set working directory" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "223EdHm_Tnod", + "outputId": "96e4f251-729d-4d90-c914-45e5c7cdfd82" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Current working directory: /vol/tensusers2/aliesenfeld/Elpaco dataset\n", + "Current working directory: /vol/tensusers2/aliesenfeld/Elpaco dataset\n" + ] + } + ], + "source": [ + "# Import the os module\n", + "import os\n", + "\n", + "# Print the current working directory\n", + "print(\"Current working directory: {0}\".format(os.getcwd()))\n", + "\n", + "# Change the current working directory\n", + "os.chdir('/vol/tensusers2/aliesenfeld/Elpaco dataset/')\n", + "#os.chdir('/Users/u517177/continuer_paper/')\n", + "\n", + "\n", + "# Print the current working directory\n", + "print(\"Current working directory: {0}\".format(os.getcwd()))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Part 1: audio extraction" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "u-YN_F_oaAgz" + }, + "source": [ + "### Dependencies\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "dXTbO87ZZ_ao" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_1354822/732630340.py:9: TqdmExperimentalWarning: Using `tqdm.autonotebook.tqdm` in notebook mode. Use `tqdm.tqdm` instead to force console mode (e.g. in jupyter console)\n", + " from tqdm.autonotebook import tqdm\n" + ] + } + ], + "source": [ + "#on M1 mac librosa needs to be installed via miniforge\n", + "#!conda install -c conda-forge librosa\n", + "import wave\n", + "import IPython\n", + "import librosa\n", + "import pandas as pd\n", + "from scipy.io import wavfile\n", + "import matplotlib.pyplot as plt\n", + "from tqdm.autonotebook import tqdm\n", + "from joblib import Parallel, delayed\n", + "n_jobs = -1; verbosity = 10" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[0m\u001b[01;34m'Elpaco dataset'\u001b[0m/ \u001b[01;34moutput\u001b[0m/ streaks_10perlang.csv\n", + "\u001b[01;34m'continuer data'\u001b[0m/ \u001b[01;34moutput_trail\u001b[0m/ streaks_1perlang.csv\n", + " continuers_for_audio.csv \u001b[01;34mstreaks\u001b[0m/ streaks_50perlang.csv\n", + " discontinuers_for_audio.csv streaks.csv zic1z6cK\n" + ] + } + ], + "source": [ + "#install tensorflow if needed\n", + "\n", + "#!conda install -c conda-forge tensorflow -y\n", + "#!pip install tensorflow-macos" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Inspect single audio files" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of channels 2\n", + "Sample width 2\n", + "Frame rate. 48000\n", + "Number of frames 73200000\n", + "parameters: _wave_params(nchannels=2, sampwidth=2, framerate=48000, nframes=73200000, comptype='NONE', compname='not compressed')\n" + ] + } + ], + "source": [ + "#Load and inspect single files\n", + "audiofile = wave.open('./Elpaco dataset/akhoe_haikom1/state_hospital.wav','r')\n", + "\n", + "print( \"Number of channels\",audiofile.getnchannels())\n", + "print ( \"Sample width\",audiofile.getsampwidth())\n", + "print ( \"Frame rate.\",audiofile.getframerate())\n", + "print (\"Number of frames\",audiofile.getnframes())\n", + "print ( \"parameters:\",audiofile.getparams())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#play audio files\n", + "IPython.display.Audio('./Elpaco dataset/akhoe_haikom1/state_hospital.wav')" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### read csv" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
uidlanguagebeginenddurationsourceparticipantutterance_strippedutteranceform_ascii
0akhoe_haikom-2-194-371425akhoe_haikom371.425371.725300.0/akhoe_haikom1/state_hospitaltx@Esîîi_
1akhoe_haikom-2-203-398335akhoe_haikom398.335398.545210.0/akhoe_haikom1/state_hospitaltx@Esîîi_
2akhoe_haikom-2-457-1245412akhoe_haikom1245.4121245.780368.0/akhoe_haikom1/state_hospitaltx@Gaîîi_
3akhoe_haikom-2-459-1247720akhoe_haikom1247.7201248.010290.0/akhoe_haikom1/state_hospitaltx@Gaîîi_
4akhoe_haikom-2-482-1290491akhoe_haikom1290.4911290.851360.0/akhoe_haikom1/state_hospitaltx@Gaîîi_
.................................
139546zaar-2-142-181303zaar181.303181.672369.0/zaar1/SAY_BC_CONV_02tx@SP1tôːtôːto_ː
139547zaar-2-367-449687zaar449.687450.145458.0/zaar1/SAY_BC_CONV_02tx@SP1m̀ːm̀ːm_ː
139548zaar-2-368-450762zaar450.762451.085323.0/zaar1/SAY_BC_CONV_02tx@SP1m̀ːm̀ːm_ː
139549zaar-3-459-440411zaar440.411440.985574.0/zaar1/SAY_BC_CONV_03tx@SP2m̀ːm̀ːm_ː
139550zaar-3-462-442380zaar442.380442.771391.0/zaar1/SAY_BC_CONV_03tx@SP2m̀ːm̀ːm_ː
\n", + "

139551 rows × 10 columns

\n", + "
" + ], + "text/plain": [ + " uid language begin end \\\n", + "0 akhoe_haikom-2-194-371425 akhoe_haikom 371.425 371.725 \n", + "1 akhoe_haikom-2-203-398335 akhoe_haikom 398.335 398.545 \n", + "2 akhoe_haikom-2-457-1245412 akhoe_haikom 1245.412 1245.780 \n", + "3 akhoe_haikom-2-459-1247720 akhoe_haikom 1247.720 1248.010 \n", + "4 akhoe_haikom-2-482-1290491 akhoe_haikom 1290.491 1290.851 \n", + "... ... ... ... ... \n", + "139546 zaar-2-142-181303 zaar 181.303 181.672 \n", + "139547 zaar-2-367-449687 zaar 449.687 450.145 \n", + "139548 zaar-2-368-450762 zaar 450.762 451.085 \n", + "139549 zaar-3-459-440411 zaar 440.411 440.985 \n", + "139550 zaar-3-462-442380 zaar 442.380 442.771 \n", + "\n", + " duration source participant \\\n", + "0 300.0 /akhoe_haikom1/state_hospital tx@Es \n", + "1 210.0 /akhoe_haikom1/state_hospital tx@Es \n", + "2 368.0 /akhoe_haikom1/state_hospital tx@Ga \n", + "3 290.0 /akhoe_haikom1/state_hospital tx@Ga \n", + "4 360.0 /akhoe_haikom1/state_hospital tx@Ga \n", + "... ... ... ... \n", + "139546 369.0 /zaar1/SAY_BC_CONV_02 tx@SP1 \n", + "139547 458.0 /zaar1/SAY_BC_CONV_02 tx@SP1 \n", + "139548 323.0 /zaar1/SAY_BC_CONV_02 tx@SP1 \n", + "139549 574.0 /zaar1/SAY_BC_CONV_03 tx@SP2 \n", + "139550 391.0 /zaar1/SAY_BC_CONV_03 tx@SP2 \n", + "\n", + " utterance_stripped utterance form_ascii \n", + "0 î î i_ \n", + "1 î î i_ \n", + "2 î î i_ \n", + "3 î î i_ \n", + "4 î î i_ \n", + "... ... ... ... \n", + "139546 tôː tôː to_ː \n", + "139547 m̀ː m̀ː m_ː \n", + "139548 m̀ː m̀ː m_ː \n", + "139549 m̀ː m̀ː m_ː \n", + "139550 m̀ː m̀ː m_ː \n", + "\n", + "[139551 rows x 10 columns]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "#read csv \n", + "df_audio = pd.read_csv('continuers_for_audio.csv')\n", + "df_audio" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Preprocess csv" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#Optional:\n", + "#check timestamp format and .div is needed to match audio extaction function requirements\n", + "df_audio['begin'] = df_audio['begin'].div(1000)\n", + "df_audio['end'] = df_audio['end'].div(1000)\n", + "df_audio" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
uidlanguagebeginenddurationsourceparticipantutterance_strippedutteranceform_ascii
0akhoe_haikom-2-494-1312920akhoe_haikom1312.9201313.280360.0/akhoe_haikom1/state_hospitaltx@Gaîîi_
1akie-1-033-99525akie99.525100.289764.0/akie1/2014-01-20Gitu4ConversationBahatiNkoiseyyoBaa_
2ambel-3-147-222600ambel222.600223.085485.0/ambel1/AM064ESD_Transcription-txt-wgoiiiiii
3ambel-4-470-662638ambel662.638663.212574.0/ambel1/AM067WG_Transcription-txt-wgommmmmm
4anal-01-098-184010anal184.010184.379369.0/anal1/anm_20160916_PO_Wolring_1Anal sp3mmmmmm
.................................
851yeli_dnye-1-457-1355836yeli_dnye1355.8361356.486650.0/yeli_dnye1/r03_v19_s2Knyââ[unk_utterance]nyâânya_â
852yeli_dnye-2-129-591369yeli_dnye591.369591.989620.0/yeli_dnye1/r03_v20_s5Kpmmmmmm
853yeli_dnye-3-113-151660yeli_dnye151.660151.920260.0/yeli_dnye1/r03_v21_s1Jamesnyâânyâânya_â
854zaar-2-368-450762zaar450.762451.085323.0/zaar1/SAY_BC_CONV_02tx@SP1m̀ːm̀ːm_ː
855zaar-3-462-442380zaar442.380442.771391.0/zaar1/SAY_BC_CONV_03tx@SP2m̀ːm̀ːm_ː
\n", + "

856 rows × 10 columns

\n", + "
" + ], + "text/plain": [ + " uid language begin end duration \\\n", + "0 akhoe_haikom-2-494-1312920 akhoe_haikom 1312.920 1313.280 360.0 \n", + "1 akie-1-033-99525 akie 99.525 100.289 764.0 \n", + "2 ambel-3-147-222600 ambel 222.600 223.085 485.0 \n", + "3 ambel-4-470-662638 ambel 662.638 663.212 574.0 \n", + "4 anal-01-098-184010 anal 184.010 184.379 369.0 \n", + ".. ... ... ... ... ... \n", + "851 yeli_dnye-1-457-1355836 yeli_dnye 1355.836 1356.486 650.0 \n", + "852 yeli_dnye-2-129-591369 yeli_dnye 591.369 591.989 620.0 \n", + "853 yeli_dnye-3-113-151660 yeli_dnye 151.660 151.920 260.0 \n", + "854 zaar-2-368-450762 zaar 450.762 451.085 323.0 \n", + "855 zaar-3-462-442380 zaar 442.380 442.771 391.0 \n", + "\n", + " source \\\n", + "0 /akhoe_haikom1/state_hospital \n", + "1 /akie1/2014-01-20Gitu4ConversationBahatiNkoiseyyo \n", + "2 /ambel1/AM064 \n", + "3 /ambel1/AM067 \n", + "4 /anal1/anm_20160916_PO_Wolring_1 \n", + ".. ... \n", + "851 /yeli_dnye1/r03_v19_s2 \n", + "852 /yeli_dnye1/r03_v20_s5 \n", + "853 /yeli_dnye1/r03_v21_s1 \n", + "854 /zaar1/SAY_BC_CONV_02 \n", + "855 /zaar1/SAY_BC_CONV_03 \n", + "\n", + " participant utterance_stripped utterance \\\n", + "0 tx@Ga î î \n", + "1 B aá aá \n", + "2 ESD_Transcription-txt-wgo ii ii \n", + "3 WG_Transcription-txt-wgo mm mm \n", + "4 Anal sp3 mm mm \n", + ".. ... ... ... \n", + "851 K nyââ [unk_utterance]nyââ \n", + "852 Kp mm mm \n", + "853 James nyââ nyââ \n", + "854 tx@SP1 m̀ː m̀ː \n", + "855 tx@SP2 m̀ː m̀ː \n", + "\n", + " form_ascii \n", + "0 i_ \n", + "1 aa_ \n", + "2 ii \n", + "3 mm \n", + "4 mm \n", + ".. ... \n", + "851 nya_â \n", + "852 mm \n", + "853 nya_â \n", + "854 m_ː \n", + "855 m_ː \n", + "\n", + "[856 rows x 10 columns]" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "#drop czech because it has separate speaker channels\n", + "df_audio = df_audio[df_audio.language != \"czech\"]\n", + "\n", + "#exclude corpora without audio files\n", + "df_audio = df_audio[df_audio.language != \"italian\"]\n", + "df_audio = df_audio[df_audio.language != \"zacatepec_chatino\"]\n", + "\n", + "#drop files without audio\n", + "\n", + "df_audio.dropna(subset=['begin','end'], inplace=True)\n", + "df_audio = df_audio.groupby(['source']).apply(lambda x: x.sample(1, replace=True)).reset_index(drop=True)\n", + "\n", + "df_audio" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### `read_audio` function" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def get_row_audio(df_audio, wav_loc):\n", + " \"\"\" load audio and grab individual turns\n", + " TODO: for large sparse WAV files, the audio should be loaded only for the turn\n", + " \"\"\"\n", + " \n", + " #print path to audio file (optional)\n", + " print(wav_loc)\n", + "\n", + " # load audio\n", + " try:\n", + " #Three options here, comment out as needed:\n", + "\n", + " #scipy wavfile\n", + " #rate, data = wavfile.read(wav_loc)\n", + " \n", + " #librosa\n", + " rate, data = librosa.load(wav_loc)\n", + " \n", + " #base python\n", + " #rate, data = wave.open(wav_loc)\n", + "\n", + " #explore fourth option: use ffmpeg directly \n", + " #ffmpeg?\n", + "\n", + " data = data.astype('float32')\n", + " \n", + " # get audio for each turn\n", + " df_audio[\"audio\"] = [\n", + " data[int(st * rate) : int(et * rate)].copy(deep=True)\n", + " for st, et in zip(df_audio.begin.values, df_audio.end.values)\n", + " ]\n", + "\n", + " df_audio[\"rate\"] = rate\n", + " except Exception:\n", + " pass\n", + "\n", + " return df_audio" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Loops for execution" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#execute as for loop\n", + "[get_row_audio(df_audio[df_audio.source == source], './Elpaco dataset'+ source +'.wav') for source in df_audio.source]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#execute using parallel \n", + "\n", + "with Parallel(n_jobs=1, verbose=verbosity) as parallel:\n", + " df_audios = parallel(\n", + " delayed(get_row_audio)(\n", + " df_audio[df_audio.source == source], \n", + " './Elpaco dataset'+ source +'.wav', #Edit path to corpus directory here\n", + " )\n", + " for source in tqdm(df_audio.source)\n", + " )\n", + "df_audio = pd.concat(df_audios)\n", + "len(df_audio)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Normalize audio (optional)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "# normalize audio if needed\n", + "df_audio['audio'] = [librosa.util.normalize(i) for i in df_audio.audio.values]" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plot `audio` column waveforms" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " 5%|▍ | 11/240 [00:00<00:00, 254.29it/s]\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABWgAAASrCAYAAAAPeFHqAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAAsTAAALEwEAmpwYAAEAAElEQVR4nOzdd3wT9R/H8dc3SZtCgbJXGQXKRvYSEUGRJYhbwIWguMUtKiKyRHEiKvoTxYWKoIiyRDYie8mmQBllbwp0Jd/fH5futE3bNMmVz/PxQJvc5e6bvnuXb773ve9Xaa0RQgghhBBCCCGEEEII4XsWfxdACCGEEEIIIYQQQgghrlTSQCuEEEIIIYQQQgghhBB+Ig20QgghhBBCCCGEEEII4SfSQCuEEEIIIYQQQgghhBB+Ig20QgghhBBCCCGEEEII4SfSQCuEEEIIIYQQQgghhBB+Ig20QgghhBBCCCGEEEL4mFLqK6XUcaXUliyWK6XUeKVUlFJqs1Kqua/LKHxDGmiFEEIIIYQQQgghhPC9yUC3bJZ3B2q7/g0CPvNBmYQfeK2BNqdWfxH4JEPzkwzNTzIsHCRH85MMzU8yND/J0PwkQ/OTDAsHyTFwaa2XAqezWaU38K02rARKKqUq+aZ0wpe82YN2Mtm3+ovANxnJ0OwmIxma3WQkw8JgMpKj2U1GMjS7yUiGZjcZydDsJiMZmt1kJMPCYDKSo1mFAwfTPD7kek4UMjZvbUhrvVQpFeHp+mXLltURER6vLrxk3bp1J7XW5dwtkwzNI6scJUPz8FaGIDn6i5xPzU8yND/J0Pwkw8JB6qbmJ3VT85PzqXk1atSIqKgolFInssrQE0qpQRjDIBAaGtqiXr16Xiuj8Ex2x2FOvNZAm1sRERGsXbvWp/vcv2M9tmA74TUb+nS/gUQptd9b2/JHhgEv8TIcWAm1OrHj6HlCg21ULV3U67vxVo4FleGhM5eIjU+iXsUSXt92YSHHov/E7N1KUkI81evlb3x9yTCwrNt/mtqOPZQoWwVKeHbXl9kz3L1hKaUqRVC2YjWf7jeQmD3Dg7s3AVC1dhOf7jeQmD3DgJeUANHLIPIGdh+7gN1mpVqZK69ueuTcZU5fTKBh5TCvb7uwkGPRfw5H7yT+0nlqNGiVr+1IhoFlw4Ez1EjaQ8kyFSGsSrbrRkdH07NnT7Zu3eouwxigaprHVVzPZaK1/gL4AqBly5ZaMvS9/ByHPp0kTCk1SCm1Vim19sSJE77cNQDVf+pE+LftfL7fwsTfGQa82S/Ad7egj2+n24fLuPadRf4uUSa+yPCxdyYxdvzHBbJtb2g7ZgFDpm/2dzHyRY7FvAv/th3Vf+rk72JIhl52+2f/UuLbzuiPfNfQ5e8Ma//eC+tEqdfkh78zrPpDB6r+0MHn+y1M/J1hwJs/DL6/DX1wDTd+sJQO467MuunAsV/x/oSPCmTb3tBx3CKe+WmDv4uRL3Is5l3lya2pMbWzv4shGXrZrZ+uoOS3N8AH+e4gOBO4XxnaAue01kfyX0IRaHzaQKu1/kJr3VJr3bJcuTz32s63RIeTJIfTb/s3M39mGH3yIgdOXfLpPnPLuW0mALt/eSPluUsJSXyxdA9Op/ZXsdLxRYZ/2IcyOfidAtm2N1S+sJl/TH41MVDOpyLvJMPci0t0cO5SYrbrKEe8j0oTGBmW4gIJSc6A+Ywxm0DIEKRumh/+zPDg6UvsO3nRp/vMLWfU3wDETH0+5bm4RAefL9kTMH9zvshwtv1VJgW/VyDb9oaSpzezcdM6fxcjXwLlfCryTjLMvbhEB2cvJeRrG3379uXqq69m586dAI2VUgOVUo8qpR51rTIb2AtEAf8DHs/XDkXA8tsQB75y5mICwTYLofbUt/rNG/1wFCnLI69N8GPJRG5oren47mIAosfe5N/CZMMSfx6ACidX8JS1OI0texk3L4Kv/4mmYlgRbm5S2c8lLDgOp2bmphh6Nwn37ZWfPPjVPtz10wB/FkOIXBu/YDc7jp7n03ta+LsofvHniFs4p0MZOOonAA6fvYzdZqFMMTt3WwOvV1hBOH4hjrAiQdht1pTn/n6zK0cr38iAx17yY8mEp85eSsBmtVAsTd30qzf7gy2ER4Z94b+CiVzRWqfcKRXIdVPOHQKgyoVNPGL9g2ssW/h0cW3GL9hNsRAb97Sp7ucCFhytNTM2xtCrceWA/9I7wz7M9dNDfi2HELk1ccke1uw7zaT++Ruewax+HXEnDq24b9R0wBhOJchqoWwxO3dYl3i0jR9//DHlZ6XUZq31pLTLtdYaeMJ7pRaBymvtKEqpH4F/gbpKqUNKqYHe2nZ+fDbmaUa/PTLdcw/Z5vBI4ncFsr9EhxOHSXuxBGqGAPO3HcNO/q5M+VKYvsDzQdO40bqeJStWAMbVtYLmzwx/WrGDE9Nf4qcVO3y1y0IpkI9D4bn85vjdyv1c/97iTM+/P38Xs/876p1C+kHPj5cxdk7ezxF3WJcy0DaHGRticDg17cYupMWov9Fa83bQ/1LWO3XuQr7LGqjH4sy3+/Phl5PTPdfDupoBx0YXyP7M3Ds3UDMcP/o5Ro0elu65Ryy/84jz5wLZX5LUTQvEv3tPmaNuqlOzfyXoRzpY/2PB0qUAXE4o3HXT39fu48T0l/h60X++2mWhFMjHofBcfnOcuvYg14xdmOn5sXN2sGDHcS+V0vfu+GwFw2duzfPr+1kXcJ/tb35ZexCnU3P1WwtpOcq4c+HdoM9T1jtx+mx+iyquAF5roNVa99VaV9JaB2mtq2Rs9feXV4N+ZIzzQ6IOHna7fP+pi2yJOee1/dV+bQ69Pl7ute35UqBmCMDxbewM6c9NlpVoba4vGQvtLwCgfLAvf2ZYfdc3DLLNovqubzx/UcJFGB4Ga74suIKZjC8yPH0xga+W7zPdsWQm+c3x9Rlb2HsisG+bzYstMeeZuGRPpucdTk1iLm61nTh1JnO3HCWEeIpxifUHzqRbfvNb0/Nd1kD9THzINoeXjz7Hf1HRbpdvPXyOaC/ecl1n6ByG/GrOMbsDNcNhQd8x1vIJew65v9hy8PQl/jvkvbpp5Gtz6Py+Z714Ak2gZgjgOLmHnSH9udO62CcX4fMq0ZH5s36W1RjuQKmCr536M8Pyu39ikG0WDXZ/nvPKyZISjLrpisCdS8HXfJHhucuJfLlsr9RNC1B+c3xp2mZizl4uqOL5zdr9Z5i8IjrT87mtm06ZPp1/9pzETgLFuMTmQ2fTLb9/3JR8llRcCQL9TmSviZxUP9NzEUNmcd24xfT8eHnKGEw7j17IdU8RrTVnLhpX0ItxiT1HTua/wCKdUue2AfCc7Re/7P+Br1bT46Nl+dqGLyrB/mTVSa7/Zz8+ZFqzZv/u+uH57FcUXvX81I2M+HMb/3nx4pQoGEt2neDg6cxjb5+5mGDuLzHnD6OdTn78cy5J7zVg8PtfM3zY83De/cXUjMLVCcbM3s4y+2C2hDzE7Z/9m255RXW6IErtd2kzv+r7zJOhRQyZxU3jl9Px3cUkOZxordlx9Hyu95PkcHLusnEuDyOW39ZG57nMImu1vqyb6bmIIbO49p1F9JqwPCXDXccu5LoHrNaa0666aSiXOXKycB4T/lTs3G4AXrFNIT7R92O5PvLdWrd3WmSWddkKd80ULBgN5zbteU/n3+a7hsv5a2hBFElkYeiMLYyatZ1/95zyd1FEDpbvPun2QvDZSwmmveMGMOqgTic/zVlI0rv1GfLxZN4Y9gKcPejRyyPUUcYv2M384BfZEvIQN0/4J93yasq8vYyF75i2gfb4+Tjm/Hck5ee5W9JPYnfs7EVizmTfgyQ6pB9NVBQWnES+NocXftlE1w+X8sWyvZlXdjpgeBhxs15l5a70XyC/XLaPZiPnc/D0JbaEPMRCuzQ2FZRalqwnK4xPcnDkXMFc1Vuy6wQdj3/HPyuW5nkbhb0SnPIOc9FodNPGxwqoLCI7Z10NL7m5Kiz844GvVqeMvw1ws+Ufnrb+SrOR85m4xM1nlR/FxiexJjrnRqA66iC8X5/oUc3ou/ZubBdimBD7LKODvoL3M19MdceGk5izlymn3Dc+drOu4aFv1hAxZFau3oM/RR2P5Z+ok5l+ThZ9/ByHjmX/xTU6pB+lOI9y1WsG/7SRbh8uY+VeN687Ew3Dwzj977f8tz/9l4bnpm6iyZt/AbApZBAfB0lPMk+cjI3nz81GHfFUbDyzNqevsxw/e5GDp2Kz3UZ0SD/aqO1p6qab6fLBUj76e5f7FwwPI/73Z1mxMybd01NWH6D5yPnsPnaBrSEDWWl/Mu9vTGSrtIpFZfGNKiHJyeEC6nE2b+sxOp/+ieXLFmS/Yjb1skLed4C81E1vXXVX6oP47I9X4T3nXXXT+CSpmwa6eyetSlc37W5ZxfO2qTQdMZ8Ps/qs8pPLCQ73daAMqquj8H59Dr7VnD6rbsUWe5hxZ55hTNAk+LCRR/sKVkmsiT5DNcsJt8tvsS7nmZ82mKpuKnzPtA20cz9+iu7T69H38+W0HrOAR79fz6g/t3H6YgIfzN9FhQ8rs/X9Xjlu53f7MPaG3EsxLjFz3T4aqb2s2XuCqOOx6Wc2dRq9A0PWfELbKfVxOFJvZVqw4xgAB88YvZzC1SlqDPmDIdM3EzFkFruP5X8svCvRkXOX+XfPKa4bt4jok6k9yLa/0YTLx/dy9FwcEUNmMXDyGgAe/349V7+10Ou9ymZsiKEM53gp6Geu+asXLB4LcbnveVjoK8Ep78+z3/+6jA05817zanE8kuS72d4DSQnnOe6x/p2b7yuByekwhsc4E+3vknjX4Y1Eh/Rjq/1BItQRQp2xLN11gklTfmR88Cc8FzSN6JB+7Js/0d8lTafRG/O4c+K/WVY8L8QZX76qK+Mzs4YzOnc7SPMHG8plbCSlPH43KP3v4mHbbP7efoyqrn1FDJnFhIW7c7c/H9v38c1c830tOo5bROf3l3DPl6uYtHwfe07E8sm8jUR8Wo25E57KcTsbQh5lX8i92ElgzqYDNFZ72H/qIjuPZqiLHDPGWys97ymu+rp2ukUzN6W/EN3NuoYaQ/5gsOuLxcX4JERmsz59kZ6/1qfvp4toMepvnpiynhd/2cS5S4m899dOyn9YmegPu+a4nZ/tI9kbci8luMif6/fSUO3jn93H2HMilgQ3jRf2DV/R7scGXIhNzXjJTuML4h7XUCkl1UVqDPmDV3416qbeHN7rSnLsfByr953m+ncXszNN/f7QmBZcOLKTk7HxRAyZRd8vVgLwwi+baDd2odeHQJi56TAliOXVoB9pv+A2WDgKLrm/QGYj630X9qppbt/hlgy3JPO7H+bkuULrpqE6lvut89Da5A20WsOaSXAq83BOpnZ8O9Eh/YgO6Uc1dYwwYvlr61EmT/2Fz4I/4inbDKJD+nFmyaf+Lmk69YfNpc8XK7Osmya3G1RWRiNu1cR9udtBmrppBc5gSXPHwvtB6X8X3a1rmLExhmrqGE6nJmLILN6dtzN3+xOFnqkaaBOSnBw4dYmvlu/j7oRfAdizL5pnbb/wv6B3+Xp5FJ1G/oZj8TsAdLGu83jbW0IeYlfIA/xpH8oDe1+g8/tLGD17e8ryjN31tTO1slMjYTezg1/BkpjaiLgv5F6Wr11PfbWflfuMClPEkFm8+UfeB6C+Euw6doHrxi3izMUErn9rNpu+fpolF29h777UD7kGlv3wSRtenLYJgAU7jvPX1qMpg5N7s9Fpz4lYnvl5I+tC0vT0XPwWfHdbrrZTTR3jg+QriismwJhwcJq8ApJJ8unEswDO/PNV+if+ncCE3xZ7tUQ5GlUeTufygzg7Zw/AnsCfSf6Z8+8yOugrQs6Ye0K3E788awyP8VEToo4Xnl4uiQvHABCq4llsf57NIQ/zzeTPGLjr0XTr3WVd7PvCeeC9oE85P+VBjl+IS/f8qVjjFlOd1yaBE6mV2MdsfxAVcn/K4zusme9ueME2lWX2Z/nutz8AePevwOrVAcbkkbuOXeCTRVHc6KqzJJ2OZpjtW8bYvmTkn9sY+P5PqGXvAXC31fPzy86Q/uwOuZ+Z9tdZP2M8XT9cmq7hNTZme5av7WpZzfdBo9Nd8NwXci9zNu6nlorh9MUEzlxMIGLILKau8ezWv8Iq0eHk4OlLTP5nHz0u/gbAsYNRDLZO5+ugt5mxLpprRsxALTHqptdat3i87c0hD7MzpD+z7K/xxrGnueG9Jbw+I+vXOy6nngcrJ+xjbvDL2BJTGxH3hdzLqjWrqK/2s2SX0YAbMWQWr5h0jGFf2XfyIte+s5ATF+LpMGYO/056joWxvdm1I7VO38Cyn+Kft+aN343n/t17ikU7jqcccxfivHdR49j5OJ7+cQPr7GnqpkvHweSb3K5vVe7rZbVUDO8kNw6s+RJGlgOH58NUmUJy7wgPG/1OrM4wUd+2GXzwy99eLlQORpWH41mfn3PtXAxE+fg95EH/Ux8wIugbSpxY7++i5MvxP0fArOfg4+bsKkSdtBKXvp/y81L7s2wKGcTvP3xC/20PpVvvPut8XxfNI2NsX3Lh234cP5++bnredW7OqW6aZQewC6ljyT9jm87ekHtTHt9mzTwv0ePW31lqf5bvp00FYMKiKI/KL64cpmmgdTo1j06cw8T3h3Jx7nDsyjiYVoc8wWDbb9xoXU9jtZdNIYN4ISh/45R2sP7HENuPxOzelPKcI8MHe9qre/ee/x8NLPspdnJjunVet33HHPsrVDm1AjBuW6u96godz2jjj8aA+/9+ku1qHy+MYv+pSyzccZyN9kE8avsTgJeDfkq3XhGVwOGoTVRRJ7CTwKDvUhvjvdkpMMseDzFrMz31xu9Zf2laan+Wo6ddH9J/vQYJsRB7zOgBWEicc/WO23v0TA5rGjrvHpnpuSc39fZqmTwR/99v3tvYxy3hu1u8t70C0izBOF5KRAdmJcpT5banTki3YPwgP5bEu5ZHZe4FNSn4vUzPKTRLd7m/jarAJBi98Wb/d8Tt+TE6pB+3W5dTYtevtB6d/rbb5LFQLdmMhwhkui082YU0vTZrW2LcrpPWkzZjjOv7Nt1LWQKvt6DDqen15rf8OP4VEv4ek/L8cvszDLDNpZ9tITXUERbbn+dx20wASqi83So9xvYlo2yT2HcgtTH1xKmsb/n7PPhD2lu3Zhr3dLRtEgvsLxIUd5KYmINEh/Tj1OLA6i3jS06nZvCk+Xz63lBi57yRMuTGQvsLPBs0nU7WTbSxbGdLyEM8FzQtX/tqbNnH87apHNmddeeDtHfq9D79NfUsBylzfEW6dV6xTWGO/RVqnzRm4o4O6UfrDUPyVTbT+m+aUTddlvn8mtaXy/Zy8PRl5m45wnL7YAbbjHrDsKDvMq27d8tKKnOSEOJ50HWHF0CCF4cUSu5FHaQynIOPb8u07vvzs74wtcD+Io4E1zll1vPgSIDTewtV3fRCvPFeDhz3rG7aafNLmZ57duvtXi2TJ+LWeXEyoc87wPe+fw+5VTfuPwDKR8/0c0nyp/y61IbMFR8P8GNJvGvBjsxz7HwSPD7TcwpYtMPHY6266qZztxzlUkLmi2HRIf3oZ1tI8b2zaD0m/cWK/aeM16ocWhB+zuJi9IX41ItaliwuhqX1UpDRMHv/tkGUx7PzkriyBHwD7eUEB2uiT3Pz61/w1cl+jAmaxFO2GW7X/c3+htf2+6jtD0ZcSN2eM0Nvx7gZz8LwMI7HRHP2svHhf/hg+p54V1uMq+hhF/enPNfPtjBX5UhIcjLs9y2cijX57S4zXD2/5r2a7mmnUzNjQwxLdxkTviRfnSp5dmtKI3xWhtm+Y7l9MBOC0n84qAVvwo/98lzUSwlJfLZ4Dw6nZtvhrCdXuXA0/e2ye1f9ke12e1vTDxTO+/Xg3Tp5LmegKXPG6IHTL2mGdzeclGB0i7581rvbdbEvepPE0VW8szGHuY7T+B3z/F2EbMXGJXImNi7nFYFHbLO8MrxJbHyS38fmjffwu7ENB/d/tZqk8S3gnZoFWyiAo1tgTGX2LPyGx39Yz8g/MzcGpFXNNbxAsjKr3yU6pB9fBH+Q7evGzXPfs7vfp3nvnb42JHDGuz4fl8jince5Z+i7zLc9wxtB3/Fs0HS36y7y0pj2VqW517aAaw98lvLcyTIt06/0S38YHsbWw6mN2cv+S3+LZjer0eAUlBhLcKzxZeWG+Nz1zDp9MYHXZ2whPsm8jUBxiQ7W7T/NLW9+xaeH7+KtoEkpFwQy+j74La/t9ynbDCbEp17o1xnqpklzXoHhYZw6uJOjF4we6ycPp6+btrEYPfNKX04dw/rWjPWTHCQ5nAyfuZVj5z07Pwes6QON/y8Yke5prTW/b4xhRdRJhs/cmvK1vWTsbsqp7C/2jA6axIqQp/kqaFy650usfC9fjWRxiQ4+XRxFosNJ1Ims7xg5fzD9eXnlwuwbu+6xZhi/9pPWMKJ0nssZaOynjR7Cd1q8fHeTI7FA66Yhq8bjGFHWOxu7ZI7Jq8OcRmNV0X2BXTe9GJfIqQueXSztb/vLK5NmXYxPcju8jS+dj/PsMzuYRB6cvIaET66BURULuFQYQ0mMqczhhV/w6PfreO237O9Saaii0z0OWvER0SH9+DF4dLavG/Lrf26f7/5+DmOAZ2N1iB+GUBEBL6AbaDcfOkvDYbNZ8L9X+DMo8xXNglZWn0q5pfL86vRXMotvMx6P/+n3lIpbl53D0q2T3NPlj02H8nz77dytR/n23/05fhE2pR2z+X3ldm75vQEdpkQyeelOyiYYvaIa7sx5QpLkK103Wo1bYcpzhp6Wf7H88wHsnJXnD7L3/9rF23N3MHNTDONnrSU6xH1j7+mv+nD6YgJfLN3D7xtj+C54bLbbDSKJFWsz9Ly9dJKTG7MeKPz3jTGZbsUIVGFOz3qoxV04g2NEuaxX2J7a0O28cAJGlSPxnTrwdnUSz3jhVlo3jXhBiRdgeBhHJz8AU/rA8DAWrNlM0vHdcGgdzv2rGL9gN+cueXjrn8Mc4zPWjvf+kCsOp/baDLwHxzSj1LsV4JCbHmNucjwZ6/kszcl+Wn2AiCGziBgyi8hXZ9PojXnUfm1OXorrNU4PhwBoYtlLXXUA2+kouHSK6esOceDUJbc9F5IcTj5esDtfY4c6jxqVU73b+PL0w6oD/HfIOO4vxCUSTPrjo71lC499vRynU+N0alod+NKj/Rw6dZ6IIbPoPSH11rD1B87whz3/d6D4e9zNpbtO0Hz4bA5/9wg/BY/y+f5LxR0whmJxOmHD9+kXbjV6BfYcnzpcRMj0+9OtUtxVr7nr83+4FJ+3ISvGzN7Odyv3Z9lTOtBtO3yeBq/PZvYXrzNTPefz/ZcgNuX25/iN6e8YK7PHyHDytN9Scrlxf/oLIsl102U7j7P9SNYXobOzPOokk1dE82oWX1hNbdc8/l6/g94zGtDu+1r8sCKKMnEHAGi09+scX578e29nNertZThHL8sKiq8cB1F/5/nCxMQle3hn7k5+WnOQifP/y7JuennyrZy7lMiXy/Yybd0hptoz362UVhBJLFmf+TvG8dVZ9/j+c/PhApv4zNtCnZ79jSdcOk/iqPCsV9iSehFNx52DkWVJeqc2vF2d+OMFM7651ZlodAb6/BaYNgCGhzF39X9G3TRmPTr6Hz5ZFOV5Jx6TDF9RVuc82WhuOZ2aFXu801C9a0xbyrxXEQ6u9mj97Udzf579bcOhlLppxJBZNHxjHnWGmqNuWt1ynCYqiuATWyDpMr9vjOHAqUv8ve1YpnWdTs0ni6I4H5f3v03taqvR2427bn/bEMOGA0Zjf1yiI918BQDXWzYwaNJSkhxOtNbU3/Y+nihBLBFDZtH5/SUpz22JOcdy+zN5Lnuy9QekJ61IZfN3AdxZtPM4D369hloqhr0hL/qtHDYc8ElrNl/3PxovcV+OkNM7aR+UfSPHG0Hfwafpb4OKS3SwdNcJujRMf2Vp38mLdHp3MbOfvpYGlUvgTIpjpO0rdiblfIUlLtHBkXNx1CgbmuO6PnUi/e1VCUlOOg39ln9CBtOs9DUpz+8KeQCioU9wVSoez7khLu2XQoWTqcEjiLCknvybvPkX20d2y3VxN8ecAzQv/Lyeuuow2N2vVz0hioiRxi3iVhz0Dsl+u/XUAWb99gPtgtI/X3ZGP2hyNtMsYhfiEjk0bQj/FNXc+mqGL9KBKKfP7fhY2P0XIdMezH69n+9l1bWTCWt4AztnfkpvIOiy0eC0Z/c26rWumrtynT8CxSqAxXU96kTW465WjJ6R8vMNs65N+dkCvB83heiTF3n/7qaZXrdg+zH+nfk/kpuPHP9NIym8DfakC1Cpce7Ka3LjF+zmowXGl5XJD7aiY93yedqO1pr6Ftd54MvrYXj6hrWNs/9H0wyvaTd6LttG9yLImvW1R6dTp1yZjE9ypLsinuTUXGfZRAI2nM4eWCz+mT6lpcXzsVLn2VNvTX7nl0WcoCROLKx+7QZW7ztN14YVGb9gN7uOXWDZ1v2cupjAHS2qULV0Uew2CyFBVo/3dXbhR5QGth86ldI48OCnL/L1mKG8+MvmTBN1jQmaBPsnUffVycQTTHQO58hkldUpbrcuZVnMVRw735IKJUL4/t/9NPe4pO5Fh/Tj1t9n89vj1+S8spdNW3eIF37ZRDO1m6gQ793tk1s1LqyDD6/iYK1+tIp1f0dPZ0vq+H9XW91fHF4Q/AK4OjklfxafjI1n19ELtItM3+trya4TPPDVaja8fiOlQoOxx59mlG0SVue7OZb33OVEYuOTCC9ZxJO3V6CW7z7JvZNWEaGOsNP+Uubby33p07Zs7DKVpn9lMbTLyd30CMq+8eDZoOnwefqe2wlJThbuOE63RunrpofOXKL924uY8cQ1NK1aEmdSIsNtk9mclPPtu/FJDg6duUytcsVyXNenMkzgo7Wm6StT2RQyiMalUs82u0Puh51wZ3A5qh3J3ZAyIcTzTfDbNLJEpzxXd+hcose6Hys2O1tdd3QNn7GJSupUlnXTCo6jRIz4CzCGk7kjh/Nuc8tupk/7geuC0z9ffvZAaHV7prqp1prdPw8l0X6BW4flb0i5gJB4GXbMIji5N3VWpg1gzTFN0fo3snfx9/QCbJeNv4cdWzfQpHzt7F+f0YWjEFoOLK7P4NN7s1y1/JFF4Lqe1W12+5TnFTAubgobDpzlywdaZnrdst0n+Gv6JJKb6B1rJpFYuzsh8aehcrPcldfkJi3flzK3zOf3taBrw7z37GxmcY0ZOunGTHXT/xb8yFUZ1r9r/HzWjbw12/qW1qnfai/GJ/Hsz5vSLb/G8h8WNEmO7tiyqeMWpPqWAx6v+7s9tdPa6J8WcYoSOLCy+IWObDtynhvql+fzJXvZcyKW+Rv3svfERQa0j6BKyaLYg3JXNz2xeCLlgd1Hz6bUTR+bOJjPxoxg7JwdvGb7Id36zwdNg4PTaPjaJC5SxOO6aUPLflqoXaw7WYfoky2JKBvKtHWHaORxSd2LDunHDb/8zoLnO+ZzS6KwCMgG2oFfr6ISZ1hg91/jbFqNlzyc5bKhQT9kuSwrP685wO8bD7Nizyl+e7wddSsWp2iwEcUfrskEpq49yPCbG1IpZgG32P5m3XGA67Pc5uGzlxnx/gc8yO+Ev/4PwUF+iFZruHiSC7aSXIx3UDHMdcb7pFW61Tq8s4hyyhiPVZ3cnakfdz2LZ70k044V87ztl3SNswCXEx3sO3kx1w3WG/cd44/gN7gqTYU6J6/ach4r6gFb1uN9Xv7zZYr0eiflcVyiA0dCHE/YZkLuOwUWuCPzx1Ns3acUf3k7xB6H4hXcjtwTl+jgowW7efr62lg+bI79cuarp+60WdYflkG9DM8XObAMWuc8A/alNVM4G1KJc9Yy1J96LXR6Da57yRgu4dO2HpUho0esfzBrYxu4szFJx3dgq9ggZdnSX8bzpnNCymPrjEdIqVoM92+PPadTs+3IeRqFh/lkf1NXbKe/9W+2OiN4f1YQHet2yXLdN9//kKCkiySUb8zOXdu5845+3NbcGG5iyZL5dEyz7vHzcZQvkVqLunwk86ynRYjn9RlbGNK9HmFFghg6Ywt3taxKk6olU9aZu2QpPZLLuvYQ/awLiNVFuM/2F63SNIx+MPt6qleuSMUSIZkanApaeXU2T69bFfIk53URVjkbcPVoBw6sDL6hNh8vjKKTZQPbQsYxbMuL9FzRjBZqJyeCKvPX63dy9Fwca/ef4dzlRBpXCWPA16t5rXMV+lxrVDsTkpzUGTqH6BDjy00v68qUfX4dPI6IIc1ornZxs/1ft+XqZ13A147uHr+PpfZnAYzx5d8qxsyIaRQ90BCCcnihB7w5iWRuvPzLeiLUca8OxZQfVfdk/Zn1v2DPepMk0yg+X7KH/y3bx8nYePaM6UGiw5nyBWuU6w6gLYfPcW3tcvQ6/jltbQtYf3gu8HiW29117ALfjn+d5pbd3DYy6ztNfOW+Sf9SgbMs9tKwE/nV9K+7slz2fB7Gu5227hCz/zvCwh3H+X5gG5pXL5lSN03ulf/Nimia3t2UkkdXcL3tLzadOgdkfWwfOx/HGx9+wkNJP3F+yFJKFPVDQ7vWcPEEF4NKc+5yIpWTG/s/Tn/J5+YJ/1BcGZP92k/vzHTRuZol9+N9fxD0KXVU5jrtlphzuf5MXrQthqnBo2lt8XzG72dsOf8ddLWupas187wKAJd+foiifSalPI5LdBBkUUbjfgDOc3t08ReUWDGWoq/sSVM3zXyhNSHJyQd/7+KJTpFYPr+eoqc9u0ux1bKBsAwaZng+7MACIOvjMdmlDb9wRpXkfNHq1J/SCto/C52HG3ddjc9bg+lA6ywW7mgOugVJR7dhq5RauoXTPmdkfOpQG9a5L2Od+7LxwM91U601Ww/7rm46bdVuBljnsNlZg7d/13Rt2DPLdUeP/wRn7Ams1duwbesmbr71Hu5qZXQOWfXvEtqkWTfm7OV0FxBjj2ae8KkksTz94wbG3t6YUkWDePOPbfRqUokW1VOHEvl75XpudP38/cr93GldTJK2cov1H66zpk7iOPSndrRqGEnJosFcVyebuxELQBNL1hcRsrM65AnidRArnA248d0kErExsH0NJi3fx9WWrWwLGc3oXU9x0/qraaZ2E6PLsnRkX05ciGfl3lOcu5xI06olefibNTx7bUXuv74JYNwZFvnaHKJDjB6tHa2pjdqfBX9ExJDWNFF7GG53P2TGw7ZZfJh0h8fvI+0QCJ3eK82ciJ+4eKCZd+qm+d+EKEQCroF2wfJ/2Gl/wL89EwrYu9OXcZ6i1FHHGPDpXM5QgiUvdqR6mVBWLviV6JAx3PnvMNbVDaXN+hcAOH/mBEt2nSC8ZAid31/KuDsac/xCPE90igSg6wdLWaPeJ0Ql8r9lO3moUwOU8nHvr8VjYclYYnVppiRdz2NvfM6MDYfJeBPWD3FPUMtuXAqubsn7IOIdrKk939yN/RbKZTq9u5jHWxbn4dIbKNXp6Uw9AdwZbJvuceNsP+sCwrjIQFv+bjspsu5zWPc5AMefPcy8cfdTKrw2WVcffCT2ODq4GE5bEaxpehNW+ud1AJzLPsCy8E2okPF6sWH4zK38tOYgIVbNYA8bZ7NTfcvHcPvIdDleSkhi/sSXaF+vMmU6Pwcnd1F01mMUBSonr7RoNIktBhL7y+OUyuO+Xwn6kVf4EUY8gw041+ZFwrobfWYHO7/N8nVTZv/N5G3w1wud87jnvJu4ZA+7jl7g1w0x/PZ4O5pVy/zuD525RJVSRbPdzpaYc0SUDaWYPeuPjE/nbmTL0ul8b/uFWkHG8T0n9jrOXupIyaKpXXP2nbzIpoNnCbZZeOO8q7EqGgiGOlPrcFvzKszfdowbF9+Zbvvv/TyPtx/ujdaaL5bu5ZFDmW+X3xzyMMs2NmLGhsqMTrqX4lzi9LoZfDbq9ZR1Sm/8POXnI3+MZkzQz5m2A1Bk5Yc8l9TXKN6Y7vDvBGj5INiLZ/u78rcS6jI3Wtexx3ofAH/t6k90yOSU5e0vL2QGtZlufxOAuq/bKUksi+3PUUQZV4O+cDbg6gXbYAEkPbSEuhMO8rg163EMm6oofrUPz3L5G0HfGXeS5MF8+0twBEbl0BvQUz0v/Qb4tgftb3/+zp6Q+3Ne0aQuJDj5Zs5yzlGS+iqGJq9OI5ai7B1j9EK/7tTPzA/5gVaTPuHPR5vT9rzxeTlz1XZKXh1L9KmLDJi8lnfvbILTqVO+CHf5YCnRIcYt5b9tOMStzbw0VngeLPl3Jf/ZH6KYMseQQ3kx9pelXKAItdVxBk86xynCmPdMB+pWLM6s2b8THTKM+zYNYXmjkrRfZszanXTxNH9tOUKdiiXo+O5i3r79Ko6ei2dwZ6M34S2f/MPvjvGUt5xl/Py1PHVze9/XTf+dAH8NxaGL8oujK48M+5Jp62O4N8Nq758YRG27McxWSXUxz7trYUm93b27dU2m5aFcpufHyxnYPIwnSq2idOfnPKqbPmyd7XHj7J3WxVTmFE9nMWeHp4rumAbDjUbecy8d59cx91GqRhNuyddWvSD2BDq4KE5b0XR104qLjY49etXnqLkvQ4kqQGSml789dweTlu8jMTGRoR42zmYnYt9PoCemyzEu0cHsz1/j2lphlOvyIsQepejvD1EUSBlIYfkHJLZ+jLNzRpHXprbXg37gdX6AN5/HBpxt8jAlbzXuThiQkHUnomnzFvDxBgdLhmR9Eb2gTFq+j11HL/Dz2oNMebgN7Wplvgi+69gF6lTIvr619fA5qpYuSomQrFvIvl68lfV/TWGMbR4tgoxjc0F8S85c7EKp0NS66f5TF9lw4Cx2m4XXThvzpTh2KazBmrrT63JXq6os2H6MG+bdnG77o76axmfP3YfWmknL93FX1BeZyvBPyGD+3t2MeW+VZFjSgxQljiMrp/L5mDdT1gn+L/Wi6em/xjEu6Ee376fdjlE8/t8zAES/1cOYfLvZvVCkZLa/K3+zq0Q6WTex22rUhebvvp3XQ1Lv4OgUv4CpXJVyEbvu66GEcZF59pcppYxhIj90NOa6pZthKST1n0vkxFM8as16/pc2agc/ZzPEyzO2X3nG9mue3s8i+/NwBMYFZT7H58WDcd9Dum4p4kqmvDGpSl60bNlSr804HicYs6leYSLipvDZPc25pnZZSoxN/ZD6OOmWdBOivZd4Bx87bqO6OkpxLlHfcoBxo40roz8MvY17bMYg1UMSH+KZl0an9mBNQym1Tmud+T6YPMiUoZvsXkscwOigr7yxuzzJav//dvmDqw/+z+hVWTFD46Kf/waj248jYnmG3uMZrnR7K8eMGTqdmgOnLxGR3Ot4eBg7gxty2/nn2Dr2DhIdxghEtpGZG/qcWmWavXJo4oM4sRi3OntRQpexBLd7jCE/LKP7jlfTXV32pb0tX6ei8xhF12eukGVSQBlC+hydTs2IP7fRrVFFvvryY7bp6nSw/Edw6wH0a1udhLjLNPo69Va8P4K70+vVn1IeJzqcDBv/Bb27deOJabs5ddFotGtXqww/PNSGZ3/eSKd65SlVNJjhM7fyacPt2IoUJ3Kx+2FY7k14hY9efZaHR3/Kr/bhdIj/gE6WjfSxLnJ7u9SfvTexbNoE3g76X+aNDT/HfWMnc+HcOWbYh2VenoXTT+9l/q5zxF84Sc2lz9De6tnYu6MT+7FHV6aE3cKH+h1O1+tL6T6pt/EX2Pk0gD4H/3U0oLllN3ZljrHrPJLmWCzQz8SU/QVOngXhvC5KCVfPQ4B9zgp0SviAzcO7kOTQlB5nND1sdtYgVhdJGZsT4I74YazVdamhjhKuThJCAl+OMY7t9cNa0Nx1K2n/hBeZPMb9GMSSYcGIiJvCO7c3pttVFdPVTd9O7MPLQamfGf9L6sHopHuppo5RjMs0sezhrdFGL+wvh/bhIdcF7LcT+3D74HeJLJ+50cXXddORiffyepD/ho4akXgfw9xctFrR+VfaHZ0CVz8O4S3SL/Tz3+Cx6z+gwsJn0z/po7qp1proU5dS74gbHsaB4Fp0P/9qjnXTPZbq1HLuT/fc8MT7iSeIt7xdN71uKMGdXuSNqf/ScPNb3GVbkvOLCsC+Js9TMegyRdZ+mvPKPqqbaq0ZPWs73a+qxKeff8xOXZWOlk0kNHmAQR0jiU9y0vCLaimvneVsx00jUju/OJ2aYR//jy7X38Ars/YT4xoDuUmVMGY8cQ0vT99My+qlqV6mKM9N3cRXzfeiLFbqLH/Gbdn6J7zER0Nf4p6RE/nTPpRO8e/R0bKR3tZ/aOqmp+jSu7cy8/vxvBv0eeaNDT/HEx/9SPTRU8yyv+bx7+fEoM0sOugg8eIZLAtH0tfm2UR2oxP7sV9XQIWE8blzOGdq9KTUA6mN8VdC3XSHsyrBJFLTctTfRfEeX9dNRYHKT4YB1UD72/qD3DozvyN5mFPbuI85SpksB/1PlqnBcfg5HE6NdUTJdOudjehOyXu/BVv6QaV8XQk2hTQnxO/+jea+eU38WBjYULkvzQ5nuHLqo0rw/5ZE8ffcGbxb/AeqJuTtVhZfGRT8Fl8kvOLvYnjs7IvHKRmaOmhcQR2Lvyzfyp1/t3O73pSkTkTpKpm+GA5LfID9FTpzWpWmaOJpfj5/Hyd0CaY7OhChjqXM3H5Sl+CsLsZHSbdxgpJMCBpPWZW3SWbyom7cZHaG9M/163Y4q3o8dIo7G501aWrZy/pi19H8hdSepFdCJbhQ8mEl+OO/d/HU8lZZvKJwi0yYQpKTHOs1HyXdymDbb6lPDD9HksOZqbElrtWThPQYlam3YUFnOGvTYW76rb43Nm86HeI/4ICukGOGLyc+nP6i2vBzxnjfGeumFdtRcuCvEJR+qAOpm7qR5jz1y9qD3Pmnf78fbQi/h2YxGXpk+qhu+v2/0fw+czpvFfuJyETPx2j3h4FBbzMp8WV/F8NjJ547Rrk0w0cV1LG4cHM01//q/vvVNEcHVjvr8k6GC/NDEx9kV8n2OItVxq7j+OH4rcTrIL5w3EQtdZgeVuPOmlO6OJd0CKOT7uECRRht+yrTsHcFqXbct8ZY1bm001mFupZDed7vKmc92lh28J+9OVe9ktq4K3VTk5IG2kIlPxkGzBAHB05dotxvd4PnY0IXKs/aphPvwSAmGXuDLpvzI/cvLcG+DJMFlIyew67lv1LtmjtzNdB2Xunzh3M5j3MAGR6Gs+m9KJzctzHnsWQLWqbGWR96eFELHrYTkOPeZmSmxlmAtb+Np/O9BT+u9pzZv3JnsPtl/bK4Oj8i6Bs4/U2658qp8zxq+zPdc2XVecqq83wcPAF/yEvjLHg+rnVWkntTOJ0BOOieCFjbDp/nnmWdcp5AsZC6V82lhPVSjuula5wFVi+dw12znZkm7ghZM4FjV79ImbASPpsk5dCZSwRPu/eKrZu+aPuZWJ3zuLEZ73hY+uf3PL6mHFsy/O2XPLqCnQu/pfr1D/mkbkqc7y4get3wMJyN7kQFF+XO9d/kvH4By9Q460N3zm3BvfYkMMGNHGZqnAXY/P3L3PD4RwW+n49/nMH1WUxsd4d1KXdYl2Z6flTQ13Dxa0gz4ohdJaa7uxSgjLpAGXWBicEfeq/AuZCXxlkgX42zAG0sxqTHDmfhHRZSiCtRwDTQvvzeBH4M9uy208LobtviPL3u2lWP8prV/QQNG/7+kSHzjvDrW8+6Xe5NaxbNpHWB76XgWDb671Y3cWVwXDztk/18FZzzzOgib1peWubvIggT+XTCO0wIjvV3MfxmeFDWY3Nnp/XCPswKru522SfvDcNR83pGD7wlHyXz3HPvfMpU+zqf7CsQpZ0QMDc6rH2CRxy3uP2WsWv5rwxdHMsvbxX8ZGub1izFv/dE5Y9lyy/+LkJAsKskfxeh0LJcPuWT/QTKBJmFUdPETTmvJIQwDa91QVBKdVNK7VRKRSmlhuTmtbGXLqebGU/kzkNZTFB1t21xthO3eFPc2SM+2c+VKjHR8y6t+TkWRcGx5GLKY8nQ/CRD88tPhrv3H2RC8McFVbRCr6Flv9vnRwR9w+iDD3i8nfxkGBefwNRsJhgR2cvYyy1ZL+tKfrGP8EkZLp074ZP9XKnOn/P8wrN8JgYmlYv54yVD85MMA9vcuXOpW7cuQCN3+Sil+iulTiilNrr+PeT7UoqC5pUGWqWUFfgE6A40APoqpRp4+vpFX7zgjWIIL8jriTtJ5Tw8g8g77fSsApXfY1EUnEolshh3IAPJ0PwkQ/PLb4YXJt1SQCUTnspvhku+fTPnlYRP5LVu6lABc6NgoZSUEO/RevKZGLgqFPPs+5tkaH6SYWBzOBw88cQTzJkzB2ArWefzs9a6qevfl74tpfAFb/WgbQ1Eaa33aq0TgJ+A3p6+uMcZ/41rJFLl58QtDbQBI1/Hoig4ISXKebqqZGh+kqH55SvD5paoAiuY8Fi+MrwqZmqBFUx4Lj91U2fgjORWKOViomn5TAxQthLlPV1VMjQ/yTCArV69msjISGrWrAmgkXyuWN5qoA0H0s7Ccsj1nEesyvPbK0SByvOJ227PYuR34RXa81uQcjwWlVKDlFJrlVJrT5xIc/tfkglmBjOx4xcue7pqvs6nIiBIhuYnGZpfvjIM0nFeL5DIk7zXTYOlgbYgOT1voM173VQm5yxQxy941gsa+UwsDCTDABYTE0PVqlXTPpVVPrcrpTYrpaYppaq6WZ71+VSYgm+mwXXJ6o9lrbOOL4shspbnE3eIkhkkC5LndWBPtqW/0Fq31Fq3LFcutVdn7KWL2bxK5JeHo1R4TD58zU8yND/J0PyyynClpYUfSyXSyHPd1KblwnNBcnixcppV3TTJIROEFSSHl9u/5TPR/CTDgPYHEKG1bgzMB75xt1JW51NhDt5qoI0B0rbgV3E9l05WfyynqtzopWKIgpbVSbvKwZl+LFXhF2z1+FD16Fh0xym9FArUxQSPL2Lk63wqCs5JXcLTVSVD88tXhhd0kYIvochJvjK01WhX8CUUXpFV3bTkgfl+LFXhFxps9XTVPNdNQeWqTCJ3zl32+CKG1GvMTzIMYOHh4Rw8mPZaZOZ8tNantNbJ3d6/BORKciHkrQbaNUBtpVQNpVQw0AfwuMWu6/0ve6kYIp9yPHFn2cgeWts3JbxCWYI8m2CKfByLWhpoC5Td4XEP5XydT0XBKavOe7qqZGh++crQ+cCsAiuY8Fi+Mux+92MFVjCRK3mum54PjfBJAa9UxUM8Ht4sz8eiUtJAW5BKJp30dFWp15ifZBjAWrVqxe7du9m3bx8YV6Yy5aOUqpTm4c3Adt+VUPiKVwZn0lonKaWeBOYBVuArrfVWjzcQEuaNYoj8SzlxY1R++wD9PHmh9L4MDPk5Fm1BMtFbQYq3l/VovXyfT4XfSYbml98MLSVlWLeCcj64Ap70Zc/3cWgvnscSCi/Lc90ULXXTgqTxrH9rfo5FaaAtWGfsnn1WSb3G/CTDwGaz2ZgwYQJdu3YFaAiM1FpvVUqNANZqrWcCTyulbgaSgNNAf78VWBQYr42er7WeDcz21vaEdyQGh+Fps1t+Tty5mChAFLC8HouhxUt6vzAiRbHQUI/XlfNpYDpMOSp7uK5kaH75ydAWFOLl0ohktlYPeryuHIfml5+6qZa6aYHSWnk8AEFej0WL1eNhFEQeFCtRyuN15XxqfpJhYOvRowc9evRAKbVFaz0aQGs9LHm51voV4BW/FVD4hE8nCRO+F3/LV7laX2s9W2tdR2tdK/nE4Ikyl/bmumwi8DiKylhDBaX6tX18sp/O8e+ke+zUKt3PDya8mO3rX058mIi4KQVStvx6IfERv+7/UNEGft2/MA+bXRpoC0rRqwf6uwgin87c+GGu1s9r3TT04sGcVxJ5poN8M9Z2Utn6PtnPlah8p8d9sp92ceMzPZegUxvfByY8n+3rRyTeR0TcD14vlzcMTvDN7zArm2xN/Lp/IYR3BUwDbYw1nAu6CEd0aX8XxVRWOrOvtOgqLX1SjgtBZXyyH1GwrM9sggfn8nztOQysNgeGn4N7f4U63dnTemSm9T9Mui3l5zidua/2ysr3A3DaVqHgCh3gWsV9Qs247ylT3PMetPnR76YuXBv/AYMSngXgIiHMi3wdgL+rPc3XY4Zmeo2udQPOF6OJH7CQnx2dAPim07/cHf86E5J680HERCZ2XJcy8dFGZy0Apjuu5ZukG9lw/w7+dLRNt83ENBXvbiX/YEJSbwCWOxqmW+/1xP4pPz+bYIz5uNRxldv3dvVtT9Mq7lOPfg8b6r2Q7vHmu1Z49Dp33k28E4AkuYAhPGSzGeOGn9LFOaXlVvncmOtolf0Kxcr7piDA/qCaXNR2YrTUcXJjlbNetsvjql/nk3JctskQagXJ4aMOyrZHFsGDc3itwTzuqfSHUTd94A+I7Mzetpnb68cn3ZLyc1RQnUzL/6l4HwBx6sq9kNYx/j1qxn1PpTIeT36aL8/deQPt4z+kb8JrKc8tbzwWgL/KD2DSmGGZXqPDW+J8KZr4B//mK0d3QDHlxtXcFj+cT5NuZlyVT5ja47+UtoOtzuoA/Oa4hilJ17P1wV1MTuqSbpsXdeq4yb3LzuKdxLuNsmSom45KvIckbTSTPORqPN7krOn2vV1/15M0j5vo0e9hXd30ddMNd+S9bjo60RjpJa5oxTxvQwgReLw2xEF+hb++jblbjhJWJAjbt00p5/lkLFekqW1nEH06jk83O+nasALvRPUiTF1KWb7bGU5tSwz4aOym0+Vaw5kFPtmXN9WNm8yO+l+j9i2Bl/ZB0dIw/Aqu0AeHQvWrea96mucib4DIG6gFUL8ZHN9GfM3ObNh7lAcateJMwvuUCrVz6q2mhOtj6TbXdtDHEDea0kFFYKRnY7Dm5Lfr5nLrkm442j2DdcWHnOszk2HrizLq4AMUvxzDH43G0+u2e2FE6sWereVuosFNT6Am98jXvrfXfYz6Oz8jtnxLdrV9i+Yzb6Rb/FjmvvUYK9+9jbaxmY+Bv9+4m51HL1DE89mO82VA+xpcV7cc5YMT4IMPUECFDgN4etsZ+nZ4MvMLBv6NqtoKBdhDW/Dr42ew2yw0rBzG7W3rcDE+iQolQjgVG89VcycB0K5WGaY83Ja6MedYf+AMzWpWIuH+yTz69UQ2BDfDEneeI5Shi2UN97SpxtzeHZi4JJyIOUZFONpqVCqbxH3Bre0aEbHCqEBP6NuY0VPP8Z3jRnZY09/G/HZiH15uUYXvV0Vw3cH3+TroHWpajrr9HZx+7D+aVajGhDcP86Q2egM3btAQ7vqOyevP0D/qaQCmJl3HXbYlbrdxZ/ww1mijkeG2JuWZsDWB1jdm/gIhjMaYNpYd/i5GQFEWCww/x/zVB2hYOYwyX1bzd5EC0g5nVepZjF6Os9r9zE/bE1h2xMpN9Svxya5O6dY9pMtSRXk8oY1XVH9tA39vO0aRYCtx37anluWIT/dvNlPazOBUbDzvrXPQuX4Fhu7pR4QltV6w01mFupZDYPNNw9jlMg3ggE925VX14r5me5NpqJ2z4NmtEFYlIOumFpvHE9jmT1ARqN6O0WnrpjU6QI0O1ARo0BIOrSWxbk/W7jzAA02v5nT8h5QOtRP7wa2QmH5z1zw6AeLfIsRqh1HeufA6vePf3L64M0ntnsO24n0u3vkLr2wqzciYgYRdjOaP+uPoeccA1MjUiz27i7cm8o43UV93z9e+10c+SfOoCVwq3YDozl/QYGp7bo4fycy3nmbVR/fS5swfmV7z5/D+bIk5R5liHk/0li93tKhCi+p3EV6yCIwyGtUrXX03T63bz+0d3fRAHfAXqlobo25avRW/P3EWpaBxlZLc0qoW5y8nUTEshAtxiVz16wQA6lcqwZzB11L/6HlWRJ2iYfUKOAdN5NGJ4/mXxoQ6YzlMWTpZNnB/y3L8flt7vl9ZlYgZRgeC5Lpps7iJdGnZkMi1NwEwuX9LRn53lJ8dndgS8lC6Yn6d1JUHm4bzy9qaXBP1Ee8FT6Stxf28TScfXk+L8Fp8NOYcgxP+Z+yrUUOw/cicHWfpvtHopDA5qQv9bX+53cYDCS+zxGn0mO1Svywf775E8y6vexrDFWWFowHtrNv8XQwhci1gGmgBujUyrgDtqtSBckf/9HNpfGdxz6UoZeG76b/xZfB7gHErskWlvzT9WMJgPgv+CIC7unXC4dTc1PE8DSuHsXLXespF/UKt1cZJ2vrEv0z+L4r+xX1ToQuvVgt2+WRXefa8fRjvPXADxJ+Hb3oBULdKOVTfH+H8EaNxNgDsqXYntQ784u9iuFfjWqhxLXagbTmjFyWhRgX9vKUk4Y5jmV8Tkv7q/D5rBDUc0Xkuwq2droZO57ACdHmTMOCjegDGh3Cv5BUfXQ4T2wNwqvOHqIjyUKwCxLopYza+qjqGoCpNmbx4K3PufAgcr1HMaqe5LZiYmkeZW9LoVXr4+vEwM80V+KCi8PBCwooE0bqGb/+2apUrxqULp1MeN61Wmo9Gj8o02Ua8DsJeNX1vtebVUscjK2a3UcxufEyUKWbnv+FduJTgoLQr80bhYTQKN84xbWpXotLzL1OpZAhBVgtOpybB0Y2QIKNh+tHrajHgmhoE2yz8MKwrypnAre0a8UavBpy5lMA/USfpcVUVTl96mfUtqjB5RGoFdZmjEa3vGwHAlIfacuJCM4a9d4TJweMyvfctA/fSqILxBajbY+OImrCYquoEdoAGN9O50iX4yGigverx73h+whjeCzZ6PvzpaENP6yoABvTrx0vF7fyx6TBv9GqI1fJtLlMIPOMS7+LFoKm5fl3DuElsDXF/W/mg4LHMv1CNfXbP5uzJi2hnhXSNPGbSp7XRMHs8uCrlE66c260Xd/ubk44Qms27M6VR01295uXEh/ndPoyjuhQ3delGp45JHD4bR2T5Yvy9fjWNjkyj4hpj2JbjD/zDmmOnudXH76VzA+MOkJ0RHeDAzz7eu/8s6Po3IXY7o39Zxmz7q4D7DAcnPM5HwcadDf26d8Lp1HRqd55G4WGs3rOc+J0/U3e1cedGkadXMWn9dgZUqIQvVAqPgA0+2VWePecczPuP3gaXz8B3twBQpmRJ1B2T4OxBo3E2AOyqfCt1Dv+W7jmbNUBuxKzWFqq1JQi4ul2E8VxRoy6TYMliGIYMkwDuDGpA3cS8N+bc3rEVdDxnfLHu8gahwPiGAJuANHXTwZvgI6OBbU+Xb6hdvTKUqwcncneR87MKwylT9xq+nL+emXc/DM4hFLUG08BmJ+aZo/weZlwEOXPDOJiWpoFWWeGRpRSz22hb07d3BtQom/5OsvqVwxjvpm4KQLU26R42qVoy5eeiwTaKBht10+IhQWx9syux8UkpddN6FUtQr6Lx3eOqamV47YWXqVAihGBb5rrpvW2rc1fLqkbddEQvQhLPckOLBoy93biTa9Z/R7iubnkO9nqZf5tW5pNRM3nCNhMw7mIN7W3UQ798oCUnLlzFo+MuMst1vkzrvwd3cVW48VnSe9Cb/Dd+PldZoo2F9XrQonIcuBpor39mMs+9P4L3XXXT2Y7W9LCuBuD2u/szuFQRpq87xPCbGxJk/SaH33rgG8f9vEju69j14r5mR4j7Men7Jb3BufKtmHW6Z36Ll6Wtzuo0tOwvsO2LK1dANdAmq/PQV4yf8hs3Ro2mvsWEl74zmJTUHTsJXKAoj9kyX8Xs0LwxFovCef4IuDpznSOUUsQCMD/yNUZHVefuLi25fV5JLEVL8QtgtSgaVjYaR9rWqQR1nuax3RU4cfQQ0yqEUbNCC1+9RWq1v5Nnlhwk6NIRxgV9Ae2eNhrCNmf/ZeaTpJtTPugK2h29ekPlSOPBfb+x7qSVX1pcDTYrlI1MWW+RaksnvdInZXInrsFdXLzjHT6Z+DEvXXrfb+XIrZAgCziyWeHFvZB0meOfPpxtA+3GLlNp+tdd+S9QxavQL0Sx92IQHSqUNJ576G/40P3t81npevsAwksW4b6u1xhP2FIr9eElUyv+tzWvAsl/yv1nQ8Q1+Sh8/jmNJmwOU5Y6ZDUTcu7uTyweEkTxkKynHaxWpmjKzxaLIsSSvtdwsM34Mnf3Gz8BqV/uPurTLGWd+6+OAKDRgAnwrXFr4n2Jr/KPq7JdJNhKtTJF+XLEKzAqcwNto6qpXzgiyxdj00Mr2JCYRPIADFVKpZaxfuUw4hreDbsnkqitPJk4mK+TdnKRIsy9ymhAaBURGBduPPFgxemEhthRu+ZSxXKKJ9uW4nL9u1h5yk7PxuHccs5C5Ac9CeMi60Ie46K2c6f9M+rHrmagbQ4NXBXNaY4OvJD4KHXUQZpY9nCRIkTETeHd7hW5Y9H16fb5xauP8fOaA0RM/4HokHuyLNv/knrwsM3zeSkGJzzO3+pqLjqs2Elg7t2lqPF777z9YtKI7reciHxvJffKP7eCR96dzNuJYympLvqhBN71VMKT9LSu5LAuw4O2eZmWd2xrXPj59O8OPK6NekCUrkwdFcN5XZTdLYdx7z/l+aR/e/p/G0vZms14F+OLd2T5YgB0bl4XeI1bVpSgSJEi/FizIs1r+u9WzroPfMKk6d1osWUkTS3mH3f/N8c1nNXFuIydx93Uw9o1aUCRoqG8HeSA6cZzlwkmlHgA5tR4hQmHanLDNU25dWEFdFARZmCc+5Mv3LWuVR5qPcUz+8M5enAPP5UtxsAuOQxh4UURLbvy0sKRXDp7nAnBH0PTeyCkJKz8JNvXTUrqzkDbHJ+U8dabb4fKrjEkH/iDjceS+Lv5dRBkhXKpt+fPC76RrgnzfVImdy7W6s7lvh/w0ZdfMeTcCL+VI7eKBlshu1PuS/sgIZaT/3s+2wbaXbcvoM70G/JfoFIR8OJe9lyw0K2i64L4fb/B+7kbZ7fHXYOoXiaUu65v7XrGfd20W6NKMM314N5fjbviAsAhXZYqZFU3zZ1Qu41Qe9ZNGlVLe1g3fe1bNHC7q2769h2NefuOxgDc19bovn3toA/hK+N82SfhdeZUMTIMCbJStXRRZo5+HEZkbqC9qnrqUG8RZUPZMmgJ/1xOIPmbQvkSqXcVVCsbStn2/WG10UD7eOIzNE2KIhEbs5oYU9Wm7UwR6O4oNZWqpUO5tH0+keowj7erQHzd3iw5EcqtTStz96Vgao27kVAuszlkEAB3Fv+eGqeXcJNlFddZNwMwy9GaJxKfoZaKoZVlJ3HYiYibwke9qtB7fod0+5wy6jnmbjlCxPfZ101HJN7HsKDvPH4vQxIf4jdHe+IJxk4Cf91TlurT898IvOXWv2mU762IwiIgG2ix2Xn6/j7M/u86Fv38ktuKo5lMdnThoK5AhRJ23jl/N03UXsLVSZ63TaWm5SgWi/HhVLJGU+YvbM7Vlm2MCn2F9x7sAiWrcWNQCDe6ttWv9VPYrFl/mH385J04/DRr7TsvP0OdoXNIqH0TH3XpCLvmuW2g3e8sT3XLcQCmOzowLqkP0SEF0/vKUaUNy9t8Rr2g41xdL7URllrX06KW+9eUG/ADTMpiYQE4eecMihYpQtFvuwLQoEIRVInSvPDCMBhhngba4OAQiMtmhVCj4azGQ9/CJ5FZrhZW0f0YTwBruvxKbr7aqWLlqFUszRMlq8E1z8A/H+b84keWQYnKhIfmYhKMGh1g31K/N84CEBzK4wlPEx3aJMvpWlUuG2i9xZNeNy1rVmBLietodN64alWpRPpbYm22zENGRBVpQsa/rCY5VGLH921Gm1cnEIfR82KdrptSWTeTHvFj6NMk0tXAfW3K86FAT9fprHYIRI3tzfm4RCJct8v+Oag9k5bXoueG9rxk+4nvkm6kXHgNiInlodtuIqxoEM+Eh1E5LASlFHcvGktifBz/6ZrYSGI7cEeLqrw8/b8sy1Yj7nsqcypXDbQfjh4DZPjy9rvHL89S9dp+qgKHlODzoU/z2eKbaLLgXtPfdveHsx1/ONsBMCLpPq6zbCKBIKYEj0m33oWr+vPfxlVUVGf4q/GH1GlfiRIVGtFCKba7upQ1fvUFiodkXR39ZcQTBfY+csUaxMC7bmPBVdcw7vuheeqNHki+TOrBVl2DWuVCGXfiLhqqaGqqIzxkm01jyz6CXOfp4hVqMdvRmo6WTbxie5Hxj90CYVXoHlSE5BuzB7avgdWSdd30vUduJcnpLPg35cao55406qbh1/PFLddD9HK3DbTndRFKqMsA/Opoz8ik+9LVTRO1lSCV3VVozzlKVGVFl9+pbTnGtQ3STPBTowNNa7h/TUT//8EXEV7ZvydO3PUHocEWin5v3ObdpHIoluKlePmZ5+BN8zTQFgnJ4Rb+oqWhaGnqDvwcxmfxywdKlMu61/ey9t+m+dT1QGgZaqXtTFqiMtwwDBZ48HsdMA9K16J6sVzMa1C3B+ycHTCNs48kPMNuWx0W+rsgGXhSN21crQxby3aj4cm5ANSpkL4ntrvz4GlLaTJe6m9UpWS2+3m1R31aLf2EJFdni4066+9Ngaxn/Ci6N67FE50igfYpz4cCt7reUrUisGfszcQlOoh43fjSNu/RDkxYVIX+m67jRT2VnxydKFWpFhyOZcAtXSldtBdPVS2ZUje9d9F7xF88x0YdSRBJbAO6NKgIZP25VCPue0oRm6sG2rdGvctbZKibTvf45Vlq2Ng3cwYJcwjMBlqXHldV4to/7+f38+0YF/Q5jS37fLr/ho4fWW55mFIq1u3yI7o0ldRpt8uSndQliAupwKYXuxBqtzLiz218+6+FjTqSlQn1qWM5xI+udSMrluLWRGPw8L5NqqW7cp4srGjWvdfA+HDxV6jBNgu7RnVP/XCq0xXaPAYtH4RPWqes18XxIRvbb0CHlqNHbAcmLIpKWbbBGckCRzNeCMrdLf4bnbVoatmTeUGn17iuVi3A8wbXRlXLslS1ooNek+169yUM4bQukXIryzuJd/FSTl/Ybp8EoeXg25uNx5WaUrZh+nH2VGmjgdJiUST0nYY+fxjfjBCVP6X7/wDjc57lvny5cuxu+DS1t2ae0RWgRs3arOi9jBbWPcSXb8Ir479mk65J3aoV+fLq692+JleuH5plA238gEXYT26B+Fio1Dj32773N3Am5ryeDxSz22h10wBerpf1hDonrRWo7MMy5Vb5h6fS8qPlzH+2TcqFrLQSXjtN8OjUam+xdgNyvQ+rRXHMVXWe/lg7Zm0+wrBeOf8dF6T/nBGpt77loFv8WD4dfBddtpyiX2vPxjkt4eoFXSTISqPwMD64uyl3taxK3/9Z+GZAa66rk/WYfD8PN27B23XsAsfOG1dkrBbFi13rptwBkuxfRwNeSRpI1dLFuCo8HHZ7VDzAO71qfLldTz3WsRb15r5IraQjzAx+Davy7UWSYU2WMmJThyyXv53Yh5eDfsr0/GZnjZQ62ApHA566PpKHO9REaxg7Zwc/rja+2HaI/4AwLpJ8r9Dt7a+i82qj0fad6vWhYtVM2y6bwxiIQYFyG7XLDfUr8G7Ze5h/vAUjg772+fjLNeO+Z6n9mSzH43VolePf1SVtZ4+uzKZhXSgWYmPUrG18/Y+FLbomSxMa09ASzQ924+JkxVKhdEx8BoCb61WGsrUzbS+sSPZ1U6tFYbVkvqjmC5nqphHtod1T0LgPTEy9mNoy8UvWX78Vqz2U6y/fyM7FqXXK07oYHyXdzptBubudeI+zkttxix1dx3Jtw9zVTetWLsXfQdfROdH9uOnJ+iW8Sqwuwky7MeTZa4kDGB30VfYb7/kBlK6VWjctE0m5BunPE5byRg9PpRSJ985En9yNj0agzZcqfT+C92fluF7Z0qWJajOayFWvuV1esWI4K2/7l6bObQRFtOXZdz5jvY4kvFxZfrzeC7dRtxucZQPtpb4zKHopBi6dMoZzyK27vguYuilA+14P8lKkd+al8IfKD35Hqw+X8ufDrdxfmBp+Lt2Y0ZdbPJKn/ZzA6GAw/bF2zNgQw4jeDXN4RcFapRvSRm31aN0e8WP44Ik76bzjLA9fm3XHm7SSh50AqFuxOB/3bcaD10Rw26cWJt7bImUoTHe+H2qMDRx1PJaDZ4w5eSwWxcjeDSHDTT6HdWnuS3iFsKJ27mlbB3IxT1thrZuKwBLQDbQAfz1/PVsPt+CWiVWopQ5zQofxqO0PxiXdzZ6Q+wp031/2b0Onb7/A4XDwnzXzvtYEt+LmROOoP+gsR1XLCQC2O6ulDM3QKf59GtUsk9KwOqJ3I57pbEy88+SU9fx7KPUEXjwkiOixN7H3RGy623DNJFPPs+5jM62zc/RNgHFF/gWgZ5NKnPqsOGXUBRY7mjAv7A5CzifwpM3z7lIPJrzIjOBhKT1zJyb14tEBD2OtmbeZgofZBlMqdg+Tg99ON/lashcTB7HM2ZiP+jQlvkhd3vn2V6Y4bsi2gfZ8s0cp0fBWsFjh6Q1wYCXUuyll+Z+OtsQTxO1h4SnPBde90d2mAlKR0uGZKiVZqd38esiigRagXbPGQGPsQKfbH6HK8Qu83LWedz7ArOm/SO6uciu1L2+BU7uxl64K1ZrnY9s241+AePCarHuDAFy05KIHhh+ULx7C2qGds1xuzdBwU7He1fnaX4vqpWhR3f+3jd2eOIJd9vtzXG+Rown2Ko2pWbEMz1TM3Vhy797ZhGbVSqY8vrpWGaLH3pT1CzKoU6F4up4jneqWz9RA+4ZjANG6EhEK2kWWocV/n7Eu5LEct72n2h25aLYwn39f70nUiVgaTaxIeXUWK066WdbwqaN3gd1NkmzITQ2pu2oyTcKLM/XU7ZmWl41oCDGZX7fOWSelgbZf4lBeDLKmNPS/ddtVPH1DJFaLovXo9BMlRpYvTvTYm9h17AK1yxfLtF2z+u3Ja9kS05g7J4ZTRx3iiC7Dk7YZjEu6m90hOR+7+fHF/a2569cJnIqNY2dI/0zLF1rb0c6xllAVnzphLEY9tYS6SJi6RIf4D6lZqVxK3fSNXg0ZfENtzl9O4uXpm/lnb+qxHRJkJXrsTUSfvEjFMHPOep+pbtplVKZ1do3pCRgNbc8DvZuGs32CUZ//zXEtK0v2YsK5s9xtXeTxRMZ3Jwzj2+CxKcPHfJ90A/c++BTBtTrl8Er3Pgh5kk9jOzE26H/UsWQ+UEcm3sMKZyM+vLspl0MbM/6bH/nFcV22DbQnGw2gbLP7jLrR4E2wfwXUTZ20apqjAyEk0LNM6lk5KPI6iMxb/drX7CXKeVw3jWzUGlZlvbxt4waAcQH3hrsep/yhcwzpXs/tBeRcy1B33Fu2EzXt5yFmHUXLVoMyefubSdl2ANVN73MNZZWVM5TA/zWxrJUKDWbNa1nXTTOq0DDri6KeCJS66eOW11mn++S43lpnHeLKNKRulfLUrZJ1JxF3Pry7KQ0qp85d0rxaqVzVTSPLF0sZJgmgXWTZTA20z6uX2KPDKa0UrSJK03zhRNaHPJrjtveX60j1HNcSIv8Cq1uCG0WCrbSMKE27yPLs1lU4S3HGJvXj7jY1uD7+XdY6jV6mCx1NU14T7ayQxdZy5+paZVj1ejfWvuH+xFCiYTdaxX3CjfHv8EDiyynPv1/hrZSf728XwUd9mqZ7XenQYKqWLsqPg9qy6tXMt5vULFfMlLfYeuSFzF2o6lUsQZlh0fxR8Uk+c9xMtfJleDfpblrFferRJmN1CGcowX2Jr/BzUkcAyl1zH+SxcRbgtrb12KBr83LiILfLH7uuFtFjb6J303DsdTuzq+YDXCbrLy7fJt2IvnGU0TgLULomNO0HIakVxsbP/Eab58x9yyQAgzfnvE4uvpzc0aIKr3Sv750KsLui3D8R7v/d6EFSzDuz+QrfsCiY70gz1na5unnazsd9mzHl4TY5r+gj4WXSf5GsF/e12/UUMLl/3sZzvKNFFWqV816DWdoKdbKZbw4g2Grhxa71aF6tFKfwbNLKmo3c9xB6yxYgt7rnU6nQYFpFlKZkWEn264rs1ZX51NGbl7vVo1d8asPRb47U3n3eqtcUDbaxaeTNfPtwe7fL27frQIf4D2gf/xFvJhoXphOUnY9UasPx4x1rMbB9+os/lcKKUL54CJuGdWH965kvLNapULxQ9RAJCTLqpjfUr8hOXY3zhDIm6R5ubh7BdfHvs8Rh3IGx1JE65rm3MuzcoAKLh3Rl86ib3S5X9XpwXfyHdI0fS9+EoSnPjwv/MOXnW1tU4csH0t9SWbJoMNXKFOWr/q1Y+UrmumlE2dB0PZwKlecyz7oeWb4Y9Ydv4q+qg3kn6W4iKpbi3aS7uSH+XY83e5IwHk54jilJRp1HN7svV/WfjG5tHcl6XYfhSQ+4XX5f2+pEj72JW5qFU6ROR6LrDyKBrHs3z3RcTULnMakXrktFGHXTIqmNQW2e/ZnGz/zmfgNm8syWnNep2jrndVx6Nw1naM8GBTZRWvWHp0CfH6HHu1CmMF+yLJymJKXe8WcLb5bNmln77J7mfDfQ87/Jgla5dPrhHOrHZX3h5+dH8tZh4pZm4ZmGjcgPd/Xcb19/hKLBVobeVJ9a5Ytxmsz1V3eqNe7o9vmxpd7MTxGFyCRwLqfl4PuH2rAl5hznLidSv1IJSocG84qGrevn0dKyi8XOJmxy1uLZoOm5HlcxTgcRotzf+mF3M85hsut6P8iL4Yf4L+YccUd2wDE4ZKnMx4O6w2hjnWdurEtQEfcNd2lnobxiFMviSprVxk2DRnHV6UvExiexYMdxRt57ferA9m5Mc3SgKHF8mdSDXk0q88cmeDlpEK2fmcLtZfPXM/Cp6yN5slMkjw81Zs2c72jBNEcHPg/+AICa4em/aE3o15zp6w6x/6/U8XWTJVW7hjpt38lxeIq0EyyZWqncX190NzO0Tww/Z1ylCg6Hlrm/PV74l1KKt5Pu5kbrunxtp1eTwBro4edBbblmzEdMDn6HngmjiXfdSBpTpC7XnHmDjpaNTA5+h6uqhFEqNHBvMg0JsrJrtNET6/j57AaoTvVgwot83XKg22W1ujwKs7Of3MedF4Ne5e1etbBU8O/QFRn9+8oN/LvnFDarol7F4hQPCWLprlZEH6pAhOUYHyXdRnV1jOaWqHyNF31aF6O0iiXJHoYNIxdnFufbug2bcvsNoSQ6nFxcuhaAzeVv5p8BPcF17fmlbvWy3FdOn3OFzZcPtGLb4fOcuhhP/UolKFvMzvAQG5tXL+U6NrPaWY9Fzqa8EfSdV+um2V3Ev/6OJxha+zCr9p2C2OOw1+iRNm5gd+JGGusM7lyXYiXdj69eJNhKkeBC2hCblRJZfAZYLNw44E3mnbpEosPJvK3HeKtfB/g160395WhBPEF8l3QjtzStzIyN8GrSwzR8ZDL3pZmJPi8Gtq/BgGtqMGiE0aC80lmfT5Nu5tvgtwGICE8/Tuq4O5vQsW45/pnZkGus6W9NdlS7hnKtPqByFn8HydJOsGRqJTMPr+JOAkEE4+fhAIafM0YetReF1g/7tywiTyY6etHP5hplNygXc1mk0f2qrMc99oevH2xFm1ET+CF4DDcnjErpmHTCXo1W58bSQu1kuv1NapULpVTxwB2cL8hqYduIbgCcu+zZsf5gwot8fc3Tbpe17tIHfn4j1+V4w/Ikr9/aEltZuQAj0jNVN81G4WFcE1mW0q4vpG/ddhUPvDSeuKodGDpkOJXDjfH3tmmjgejxBPcHUkaDE59M/zjhcfbenMOIz32moJTirlZVGXlLIx7pYPQm0ah0PQyU9s+kCAHnzslGD8VsWCyKiLKhNAoPI3rsTXRrVInFHX+hb4L78aCKRV5DtUen8c6zD/Fx32b8PKgtN9QrTzUvVCaVUlgsip43GD0d5jlbMs/ZinsSXiGx0xtQP/1s4mFFghjQvgZWlTlv24DZtG3g2fg7V5JLjVN7gMS/4NvxpYVhY1DeruoHkloNWuS8ksmULxFCsQo1uTFhHPEE80KXOvDKIcKfX5ZuvTKB1jjbfzb/S+oBwMdJt6RblDxDcZ+EocTo9MMxRDkrc0v8CG6JH8EiZzOwuK+atKqZ/nUntGe9HsYNvAlL4zsgwBpowbhTp1VEaYq7hgv4cVBbqjw8hbjIHswdfh+ntNHr+G+bcYvkgwkverTdj5JuS/k5ERvDE++HAakzwCt3dyQ8aMxaP7hzbV7oWpfO9Yw7CjSWbGfIvtI1qFyCa2uXSxlLd/jNDXlqyDjiql7L0y+NoUk9Y8zWfdr4sv10wpNZbiutwYnpe4w/kfA0O7tnHh84nTu/wWJR3NIsnLdua8xT1xtf/JxYsNusKUM2WRwJHr+/Qu3uH6D7uGxXUcqom9auYAzXcVPjSvzb+bcs66ZJVdpS87GpjHrmET7s04xfH2/HdXXKub3LILeS66b3du8IwCxHG5Y6m3B3/OvEd3gVmqafrbyY3cbdraphczO5mXXAbK6+Km93nRRmSS1TG0QvP+tmbgtRYI7pkgD8o/Ix1FiAaNWs8NVNyxazU6NmJJ0T3uUSIQzqUBNeiaHci2vTrVeqaIDVTQf8xadJxh0nk5K6p1uUPH76rfFvclSnH0bihA6jV/wobo9/w1U3dX/hsknV9K/b4PRsQrc37++B7apb8zbniSjUzF/jLlGZkIHGdBRlq9SC45AYfjX/9ZzO7AnLuTa+BsvszwLwbXAf7k8wKrex98/nlYVn+DfqFOP6d+bqyTX5N+QpAJ59bigRWfTAPNL8OSoV1enGDjU4Xf9N/8XSYgvcK0g+1fDWPL2sY8cudOzYBYaPTnnuZEh1Xjh/Fw0r9KZbeOots21qlqFNzdyNw5iTf86V4cW4r7hMCL88ejUtqvXI9lZ7G96Z4fdKULT3e9CwB2gHRYqXgqHHyW62TeE9R0LrU+nidk5W8XwMrUD13l1NU3r2FSYNK5dg57ELAJQrbgd76i1fG5yREFQU2j/nr+K5F3ENo5POMjrpXgCecrPK1qDGXBP/MXaMBqLv2hxi4KqKfHB/Bx76dq2bV6TKeHaIb/kYrHs753KVy7rHZyCyVW2J7V5j+tC4UnXh/Doadbqb72yvs+j3rQxNfJBRQcawF6OKDmHopbEkaCtxfadz089nuXA5nlfvuIa7pzfkZ/tInFh4+MV3sKXpKadUal3lQoO+FK/RCqq3y1ASo8enLkRDE/hMsfKEDPwTgDIVI2APnC3Xgqg+s5n5/hKWxDVmU4gxhNIUay/6OYx6bGz/BYxccoaF24/z1v2dafNtJKtCjAbdF58bkmXd9FCLl6kSfBEa3pLuee10ZZjx6Aky51iyXlc/b5M7Xd3+eq5uf326ummCpSiD4p6kWvle9KicWjdtXq0U3wzw7m3KWy6G8VjcV1zGzs+D2tIyoof7yYpcrEhnEU8V7T4S6nSCpMsUCSsrdVMfSipTF06vYn+452OOBqpRtzSCbf4uhffVq1iClXuNCdLLF7eDPXUIgZ26KgQXh46v+Kt47lVrwztJJ3knyRg/N+M9WiFBFqKs9Wkb90lK3fTn9sfpvzyMN/tcy+CfNma7+YzDN5W69mH45+Us1k6jSt6GKBOFn/kbaNM4VPZa7owfRr0KXbi5Shi/P3EN70/9Cy7AeV2U7+19GHbeuIISXbM1Y8OT2HfyIo3Cw/hnzH0wwvg6mVUFGKDSzVl0YdfuK8HKemXd7ucLZ/v+yeLP/uOlxuE5r5xPT3SqxY+rDzDujsa0iiid4/pFrRqpB8OxXj8Qd+ZQ9oOpW4OgbrfUx3Ixw2cOVelJpZ3bORccWLdP5UUxu42EkLIEx7mf0dzsOtcvz50tUm/N/PeV60lIckKZu/1Yqqzd27Ya36884HZZ8kQP5+MSaTz8LwBa3fIkP7Y+T6PwMCb0a5btbPBBNgsJ2kqwqzdYlV6vknhhC6u3R/Nk4lNsSDPJQ0TclNQJtwJocpTc+r3UA3x9og6Pl2nCvfXLU7NcMeZ8bfSE/dPRhmVB1xARN4Vra5flu3ptmPF8PGcuJRBZvjitSvaE70fiRBGe8TZm1xeKOEtRit810e2+k+8A0ua62SrgnCzbgjvih1Gl3HXcUr4Yfz7Vng//WA1HjeWfWO/n1Yt9AYiOaMnwyg7uOX6BxlVKcsNb98KbRgNtdnXTKr1edft8cobJnQcuajuhKh5lkbqptx2/ZwGL/7ePGS2rFfi+7mpVlXf/2sXwXg086phQIdQKlwu8WAHv+K1TuXx0Vw51UxvU6ZL6WOqmPnO8ag/CT6/ihN2z4SgCWZFgK5eLVaNIrPv6kNm1jiidbiLi1a/ewMUEB5S904+lytpzN9bh/fm73C7bMdLoVZu2btq05yN83+wcDSuXwG6zYg/Kuh5ksyrO6aIpd6hE3PgoSWdWs37LFh5OeD7lYixkqJvaAqynsQgYharWbVGwRtdLmdm7SdWSjLnVmKDhPEUZ2qsRANfWLgtAqN1GI1cPzJwmIEq641scaW4PzCgh1GgsnBqS/sSksugOL3KnT6kfU36OLFeM6LE3eeV2sZxUKVWU6LE3cWdLzyoLJ1q/BMApS9mCLJZbSqk7lVJblVJOpVTLnF9RcCq06En1zjnPiCnS81WGJTs9Te24b7muecOC2oVPBb+wDV475vH64xLv4vukzJPgBJLICkavhAevqZHu86lSWBGql8nfGNsF6ZEOxi3VVUplPeZaiZAg/n3lela+cgNKqZTP4Z6NK3Nt7awn6gsvWYSG8UbP0eRbIYP6TeGexNc4R2ovjuRhAP5xFIK/b4uN9boOFovRS+OayLL0bGyMl3lOF+PZG43b55tVM26xK1PMTmR5o7d18jClmXpPuiTe9CHBjy3NctfRpa8FYHuZLlmuI3KmUKzV9bC4LhQ0Cg/j3TubAEbngTdvMX5OnqW7SLCVxlVKGq/Nofdy0l0/kPTA7CyXO4oYF5a/st3lKktyoQpV9d9vHqs6I+XnKqVCiB57E03zOc6sJ8oXN/bVP00DSXb0tc8DZDuZbUEJpLpp+SZdqd7V3b0dIju+yrDMtQ8bddPWhWN4gCLPrIPXjnq8/ujEfkxNyvsE175Qs5xR/xx4bY10vfbLlwihRj7nfylID1wdAUCJkKwv2JcICWLVqzewYogxwVuj8DCUUnRrVJFOdbOYQ8f1uhbxxoXui9q4oGO76yvuSngjXd30kYRnAJjvMP8QHqJg5btbiVLqTmA4UB9orbXO/h7FAnRny6rsOHqBZzvXSfOscfKwWRQd6pRj0QsdqVgi9xUUW6Pe2S63FilBRNwUWlcyKsMOZcWqHSm9VET+fPVYF+LfLYk94SwE8Li+ZWo0hpVAWBU44/MefVuA24DPfb1j4TU+ybB2xRLsHpv9Oc1UctnD5RPHLQDcWwBF8dZn4qMdatEqorRHPfcDSZliRo+Ah6/NftztSmF5mzQjERu3xQ/ngK5A8i+2UlgIx84ZPRccWrHI2YzP72tBuZIzoXTuL5IGUr1mzK2NCC8ZQoc0DdfJtYo6FUJp1agSc5+5ltrlM896bHHdzqGyaIwLavVgtvu+XDKSiLgpPFe6TrbrBaJAyrDHVZVYt/8Mz3SunfJcctVQKU3nBhVY8mLHlDFsc8PWIPtb9C3BRYiIm0KDSiUYAsbQBknxBGUzyZjw3Af3dyD2w6oUu3gw5U66QFSqan0AEkIrU+TiXl/vXuqm5ueTDKuVDS1kddPc9ZD8n8M4n99VAEXx1mfifW2rU69iCVrXMFfdNHniy8c7ZT8+bIU8tBEBJGHjlvgRHNZlWO16LrJ8MaKOx6asM8/ZmkkPtKRSsalQRjrwiax5476/gPngDQmyMtrVYzaZ0270slxhacFtkO3VnW+SbuRgyVYMzcO+61Qoxms96tO7mdGzxfroMohakIctCXeKBtvAZoUEAroSXLKI8WFcpkQxeHA7+PA2Qq31dsi5x40IXJJhoeCVz0SLRZmucRaMc3XyUAYFZb2uw5IXO6Y8/vOp9hw5cxEmGY/H3nYVXRtWzM8uAqZeU75ECG/2bpTuubPFjS8Y+4teRSuMMeHcKVquBrMdrYmu9zCP52Hf97WtTmxckjERCMD9M+F8TB625BcBk2GwzcLIW9JnqK1GY+wiWnEzZNsr/suk7hwObciwPOw7vGQRhvVsQLdGxvFQ9JH5sGMWtpBiObxSeCIkyAr2ILhIQNdNi9uNxoCwYqHw6E58Oaaq1GvMTzIsFLzymaiUMl3jLBifwwVdN92oI/n7udQe0NMevZqDpy/Dl8bjUbc04ob6FbLdxty5cxk8eDBAI6XUEK312LTLlVJ24FugBXAKuFtrHe3FtyECQL4baAP9pF0krAxt4z6mRcN63JbDul1f/J4SRfL2K1FK8XCHND2GKjQ0/gnvCW8Ju+cF9pgtVVrB1U9C28egRGV/l0ZkxyaTpAjvC/TPxMIibYNWmWJ2yoQGk1C2IVz7LH2a5G8MyEDP0FbjGtov+5D+tTtku16p4kW46tnfuTEsb+e6YJuFwWl6fVIzsG+9TCvQMwwuEkqbuAnUj6zBzTmse/NLkylqz3t1fUD7NLfBl6tr/BPeU7kZnN4LwYF7ey/l6sE1g6HlACier4tXQggTCvTPxMIisnzqxc+SRYMpWTSY+ArNUK0e4t6W2Y5+jcPh4IknnmD+/PnUqlVrK9BXKTVTa512urmBwBmtdaRSqg/wNhCYk1KIPDPvzBkeKlvMzqeP96Jexcy3/2VUMY9fYoSP3PEVnNgBIWE5r+svFgt0HZ3zennUuXNnjh49CtBQKbUlzaLXtNa/e7INpdQgYBBAtWoFP5lFQHp8JRTxzxVgb2QIkmN+rBhyPXGJDn8XQ+TBkhc7YnH3BUMpgp9c4fsC+cEN9StQ5KFetK2R8+RAVUsX9UGJRG4VDbbx+RO90n2Zy0r5PN5yKXzk5gnQ5lEoEcCTbioFN44osM1L3dRLnlgN9oKfX8MdqZv638pXbiA2PsnfxRB5sOylTlneRGF/bLFH21i9ejWRkZHUrFkTQAM/Ab2BtA20vTGGqgCYBkxQSimtA/gWDpFrHjXQKqX+BtxdcjXFSbu5a/IMYXL2YlDFr/ML+N3ff/8NgFJqq9Y6T78MrfUXwBcALVu2vDJP6OXr+23X3sgQJMf8qJxxRvtcki8y/uOtCdLMnmG7Wr6fiDLQmD1DX0wmJXwguChUbe3vUviV1E29xI+926Vu6n/57Shm9s9EM/PGxfCYmBiqVk03KfkhoE2G1cKBgwBa6ySl1DmgDODziW9EwfGogVZr3dkbO0t70lZKnVBK7U+zuCzm+eMyS1ndlTP7/vW5sG7dupMZMsxqn/4SSGUB75bHKzmaIMOC5O/3WZDHor/fmzeY4T1U11o3ynk19wrRZ2IyM5ZZMjSYpaxu6zWSIWCeskrdNHDKAlI3DTT+fp9SN81ZoL8P+UxMzyxlLgWUmDRp0n7yeRymbWQH4jM02JuJWbJzJ89X3Pw2xIHWulzax0qptfm5YudLZilrQZczY4a+2GduBFJZoODLo5S6FfgYKAfMUkpt1Fp3ze41gZ5hQQrE95mXDMHc59OsFIb3kBuFIUMzltmbzJyhWcrq63qNWX4vYJ6ySt00cMoCUjcNNIH4PqVuml5heR+eKAwZmqXMSqmrgeHJx5ZS6hUg48ysMUBV4JBSygaEYUwWlk6GRnZTvH93zF72vL7W4oWd36qUOgRcjXHSnpffbQohck9r/ZvWuorW2q61ruBJ5UkEFsnQ/OQz0fwkQ/OTDIUIDFKvMT/J0PzkM9EU1gC1lVI1lFLBQB9gZoZ1ZgIPuH6+A1go488WPvnuQau1/g34zQtlEUIIIUxNPhPNTzI0P8lQCCGEMMhnYuBzjSn7JDAPsAJfaa23KqVGAGu11jOBScB3Sqko4DRGI64oZPw2xIEbX/i7ALlglrL6o5yB9LsJpLJA4JUnK2YpZ34V5vdZGN5bYXgP+WHG92/GMhckM/0+zFJWX5fTLL8XME9ZpW4aWAKtPFkxSznzqzC/z8Ly3grL+8gLM75305RZaz0bmJ3huWFpfo4D7szlZk3z/t24IsuupFe0EEIIIYQQQgghhBBC+EeOY9Aqpb5SSh3PavY3ZRivlIpSSm1WSjX3fjFFfkiGhYPkaH6SoflJhuYnGZqfZFg4SI7mJxman2RofpKh+UmGIpknk4RNBrpls7w7UNv1bxDwWW4LoZTqppTa6fqDG5Lb13uDUipaKfWfUmqjcs26ppQqrZSar5Ta7fp/KdfzWR4gSqkHXOvvVko9kNX+clm2TAdsLss2GSPDklmU7SngQdfPOwigDJVSVZVSi5RS25RSW5VSg13PD1dKxbjy2qiU6pHmNa+4yrFTKdU1zfNuy6iMwbhXuZ7/WRkDc2dXpgL9W1FKtXBtP8r1WuVaNJkCPBYD4TjMKz9mkluTuQIzNFE+nphMIf9MLGR5uTOZQp6hqwxSrzEEVL0ml2UozBmCkeMEjIlRMp0ngH3AQIxbPgPmM1FJ3VTqph7wYya5NZkrMEMT5eOJyRTyek0hy8udyeQjw5zyUUrZXZ8lUcr4bInwXtHzx4Oy91dKnVCpn60P+aOcGamCalTXWuf4D4gAtmSx7HOgb5rHO4FKnmzXtb4V2APUBIKBTUADT1/vrX9ANFA2w3PvAENcPw8B3nb93AOYAyigLbDK9XxpYK/r/6VcP5fyQtk6AM3TZpCHsjUBEtyVDTgOvO56zRzgUKBkCFQCmrt+Lg7sAhoAw4EX3KzfwLV/O1DDVS5rdmUEpgJ9XD9PBB7z598KsNq1bnIe3Qv6WCzIDH3xz5+Z5KGsV1yGZsrHnxkGSo6FLa8rMUNf5JjPsl2x9RrJMFPZDgDb3WS4GmNSm76u13bPzbFYkBkidVOpm3pWfr9lkoeyXnEZmikff2YYKDkWtry8maEn+QCPAxNdP/cBfvb1+8vr3xbQH5jg77K6KXumelCG5W7/DnP650kP2pyEAwfTPD7kes5TrYEorfVerXUC8BPQ2wvl8obewDeun78Bbknz/LfasBKjB0cloCswX2t9Wmt9BphP9ldCPKK1XooxU19+ytYBiM1YNteyosAibfwlfYvxRxQQGWqtj2it17t+voBRic+ubL2Bn7TW8VrrfUCUq3xuy+i6OnY9MM31+rS/y9zwyt+Ka1kJrfXKNHl4Wp78HIuBfBzmVSBkkltXUoZmzMcThfUzsbDm5U5hzRCkXuMpyTAHXsqwK7AMcLg7TwBBGMdi8nkiID4TpW4qddN8CIRMcutKytCM+XiisNZrCmte7mSXoSf5pP1dTQNu8EMvYXcC9W8rR1nUg9LK6u8wWx5NEubqAv2n1rqRm2V/AmO11stdjxcAL2ut17pZdxBGl2xCQ0Nb1KtXL8d9C++Ij48nKiqKuLi4k1rrckqp14HLwGKMlv1btdbLlVLXYvRY6CYZBp6MOaZd5umxKBn6lzcydC2THP1EMjQ/ydD8pF5TOMTHx7NlyxaH1tqWIcOxQJzr/wp4GSiC1GsCjtRNzU8+E81PMjS/vGaolLoDo47zkGvZJKAXcEAy9I9169adBFbh4XGXls0L+48BqqZ5XMX1XCZa6y+ALwBatmyp167NtmzCi6Kjo+nZsydbt27d72ZxPOkzDEEyDEg55OjRsSgZ+pc3MgTJ0Z8kQ/OTDM1P6jWFQ3R0NDVq1EjMYnHysXjI9VjqNQFI6qbmJ5+J5icZmp+3MsS4yHlZa/2kZOgfSil3GXrEG0MczATudw2C2xY4p7U+4oXtioKVfFDHABpXhkBH4JJkaEpyLJqfZGh+kqH5SYbmJPUa80ubYRVcx6Lr50TkWDQjOZ+an2RofpKh+WWXYW4ab4Xv5CmXHHvQKqV+xKjcllVKHQLewBgTCq31RIyZVXtgjKd0idRZc0WA6Nu3L4sXL+bkyZMAjZVSTwG3Azu11keUUkeAixgZlgde819pRVbc5DgQORZNRTI0P8nQ/CRD85N6TeGQnCMQopSKwZggZCfGuG3ngVMYk7hMwpj47Q7/lFRkRc6n5icZmp9kaH75zHANUFspVQOjAbAP0M+HxRfuzQSeVEr9BLTBwwsjHo1BWxCku7V/KKXiMW4VG621/tr1XEtgMsbYXnOAp7QHfxiSof8opdZprVvmdzuSof94K0OQHP1FMjQ/ydD8pF5jfpJh4SB1U/OTz0TzkwzNLy8ZKqV6AB8CVuArrfVopdSIFi1avC4Z+p5Sah3QCpiAMTHrJeDBnMafBe+MQSvMZUvGA971h5JpAjghhBBCiAAn9RrzkwyFEEKIPNJaz8boZZv2uWEtW7Z83U9FuuK5Lio/kdvXeWMMWiGEEEIIIYQQQgghhBB5IA20QgghhBBCCCGEEEII4SfSQCuEEEIIIYQQQgghhBB+Ig20QgghhBBCCCGEEEII4SfSQCuEEEIIIYQQQgghhBB+Ig20QgghhBBCCCGEEEII4SfSQCuEEEIIIYQQQgghhBB+Ig20QgghhBBCCCGEEEII4SfSQCuEEEIIIYQQQgghhBB+Ig20QgghhBBCCCGEEEII4SfSQCuEEEIIIYQQQgghhBB+Ig20QgghhBBCCCGEEEII4SfSQCuEEEIIIYQQQgghhBB+Ig20QgghhBBCCCGEEEII4SceNdAqpboppXYqpaKUUkPcLO+vlDqhlNro+veQ94sq8mPu3LnUrVsXoJFkaE7JGUZGRgJUzLhcMgx8kmHhIOdT85MMzU8yND/J0PykXmN+kmHhIOdT85MMBXjQQKuUsgKfAN2BBkBfpVQDN6v+rLVu6vr3pZfLKfLB4XDwxBNPMGfOHICtSIamkzbDbdu2AZSWDM1FMiwc5HxqfpKh+UmG5icZmp/Ua8xPMiwc5HxqfpKhSOZJD9rWQJTWeq/WOgH4CehdsMUS3rR69WoiIyOpWbMmgEYyNJ20GQYHBwOcRjI0FcmwcJDzqflJhuYnGZqfZGh+Uq8xP8mwcJDzqflJhiKZJw204cDBNI8PuZ7L6Hal1Gal1DSlVFWvlE54RUxMDFWrpotEMjQZNxkmIBmaimRYOMj51PwkQ/OTDM1PMjQ/qdeYn2RYOMj51PwkQ5HMW5OE/QFEaK0bA/OBb9ytpJQapJRaq5Rae+LECS/tWniJZGh+kqH5eZQhSI4BTo5F85MMzU8yND/J0PwkQ/OTumnhIMei+UmGVwBPGmhjgLSt81Vcz6XQWp/SWse7Hn4JtHC3Ia31F1rrllrrluXKlctLeUUehIeHc/Bg2k7QkqHZuMkwGMnQVLyZoWtdydEP5HxqfpKh+UmG5icZmp/UTc1P6qaFg5xPzU8yFMk8aaBdA9RWStVQSgUDfYCZaVdQSlVK8/BmYLv3iijyq1WrVuzevZt9+/YBKCRD00mbYUJCAkBpJENTkQwLBzmfmp9kaH6SoflJhuYn9RrzkwwLBzmfmp9kKJLZclpBa52klHoSmAdYga+01luVUiOAtVrrmcDTSqmbgSSMwcX7F2CZRS7ZbDYmTJhA165dARoCIyVDc0mbocPhADgtGZqLZFg4yPnU/CRD85MMzU8yND+p15ifZFg4yPnU/CRDkUxprf2y45YtW+q1a9f6Zd9XMqXUOq11S29sSzL0H2/lKBn6jxyL5icZmp9kaH6SoflJhoWD1E3NT45F85MMzU8yNL/8ZOitScKEEEIIIYQQQgghhBBC5JI00AohhBBCCCGEEEIIIYSfSAOtEEIIIYQQQgghhBBC+Ik00AohhBBCCCGEEEIIIYSfSAOtEEIIIYQQQgghhBBC+Ik00AohhBBCCCGEEEIIIYSfSAOtEEIIIYQQQgghhBBC+Ik00AohhBBCCCGEEEIIIYSfSAOtEEIIIYQQQgghhBBC+Ik00AohhBBCCCGEEEIIIYSfSAOtEEIIIYQQQgghhBBC+Ik00AohhBBCCCGEEEIIIYSfSAOtEEIIIYQQQgghhBBC+Ik00AohhBBCCCGEEEIIIYSfSAOtEEIIIYQQQgghhBBC+IlHDbRKqW5KqZ1KqSil1BA3y+1KqZ9dy1cppSK8XlKRL3PnzqVu3boAjSRDc0rOMDIyEqBixuWSYeCTDAsHOZ+an2RofpKh+UmG5if1GvOTDAsHOZ+an2QowIMGWqWUFfgE6A40APoqpRpkWG0gcEZrHQl8ALzt7YKKvHM4HDzxxBPMmTMHYCuSoemkzXDbtm0ApSVDc5EMCwc5n5qfZGh+kqH5SYbmJ/Ua85MMCwc5n5qfZCiSedKDtjUQpbXeq7VOAH4CemdYpzfwjevnacANSinlvWKK/Fi9ejWRkZHUrFkTQCMZmk7aDIODgwFOIxmaimRYOMj51PwkQ/OTDM1PMjQ/qdeYn2RYOMj51PwkQ5HMkwbacOBgmseHXM+5XUdrnQScA8p4o4Ai/2JiYqhatWrapyRDk3GTYQKSoalIhoWDnE/NTzI0P8nQ/CRD85N6jflJhoWDnE/NTzIUyWy+3JlSahAwyPUwXim1xZf796KywEl/FyIXSgElJk2atB+om58NSYZ+kzZDgIZ53VAhyhDMlaPXMoRClaOZMgQ5n7ojGRokQ9+RDDOTDA2Soe9I3dQ9M+UodVP3zJQhyPnUHcnQIBn6R54z9KSBNgZI25xfxfWcu3UOKaVsQBhwKuOGtNZfAF8AKKXWaq1b5qXQ/ma2siulrgaGa627KqXWIhmaruxpM3Q9PsQVniGYq/zezBAKT45mK7ucTzMzW9klw8zMVnbJMDOzlV0yzMxsZZe6qXtmKr/UTd0zW9nlfJqZ2couGWZm9rLn9bWeDHGwBqitlKqhlAoG+gAzM6wzE3jA9fMdwEKttc5roYTXpWQIKCRDM8p4HJZGMjQbybBwkPOp+UmG5icZmp9kaH5SrzE/ybBwkPOp+UmGAvCggdY1vsWTwDxgOzBVa71VKTVCKXWza7VJQBmlVBTwHDCkoAosci9Dhg2RDE3HzXF4WjI0F8mwcJDzqflJhuYnGZqfZGh+Uq8xP8mwcJDzqflJhiKZ8leju1JqkKv7telI2b2/LV8zc9nBe+WX34P/yLFokLJ7f1u+JmX3/rZ8Tcru/W35mpTd+9vyNTOXHaRumszM5Zdj0SBl9/62fE3K7v1t+dqVWna/NdAKIYQQQgghhBBCCCHElS7HIQ6UUl8ppY6rLGZ/U4bxSqkopdRmpVRz7xdT5IdkWDhIjuYnGZqfZGh+kqH5SYaFg+RofpKh+UmG5icZmp9kKJJ5MknYZKBbNsu7A7Vd/wYBn6VdqJTqppTa6fpjyjROhlLKrpT62bV8lVIqwuPSFzAPyt5fKXVCKbXR9e8hf5TTnQwH+WQyZJj2IAf2Ai2QDAM5Q8iQY8YTNca4NW6PRcnQPyRDg2SYbnuSo49Jhqkkw5T1JUM/kM9Eg2SYsq5k6AeSoUEyTLc9ydHHJMNUhSjDjMvTZag8bVTXWuf4D4gAtmSx7HOgb5rHO4FKrp+twB6gJhAMbAIaZHj948BE1899gJ89KVNB//Ow7P2BCf4uaxbl7wA0T84tY4ZAD2AOoIDfgCjJMLD+ZcwwY44ZMmwLHHN3LEqGkqFk6P8Mc/G7kBwlQ8lQMix0GXozR8lQMpQMJUPJUD4TzZyjZFg4M8ywPGOGqzzZric9aHMSDhxM8/iQ6zmA1hgNf3u11gnAT0DvDK/vDXzj+nkacINSSnmhXPnlSdkDltZ6KXA6m1V6A99q468nCAhWSlVyLZMMA0BuMtRarwRCgUtplifnKBn6iWQISIZyPvUzyTCFZGiQDP1EPhMByVAy9DPJEJAM5TPRzyTDFFdShiXTtLdlyaNJwlxdoP/UWjdys+xPYKzWernr8QLgZa31WqXUHUA3rfVDrmWTgF7AgdDQ0Bb16tXLcd/CO+Lj44mKiiIuLu6k1rpc2txcP1cEHnXlJhkGqLQ5AqtIf+ydxMhtkuvxAuBljKtxkmGA8FKG9wEDgOIAkqNv5SVDOZ8GFsnQ/CTDwiE+Pp4tW7Y4tNY2N98ppF5jAlI3NT+pm5qffCaan2RYeKxbt85dhimZZfdamxf2HwNUTfO4ius5dxYDl7XWT7Zs2VKvXZtt2YQXRUdH07NnT7Zu3brfzeIYjA/ZZJJhgMohx3igQprHyTlGZFhvMZKh33gpQ4CtWusnASRH38pjhu4sRo5Fv5AMzU8yLByio6OpUaNGYhaLpV5jAlI3NT+pm5qffCaan2RYeCil3GXoEW8McTATuN81CG5b4JzW+ohrWW4ab4Vvpc1mJsaHbIxkaCoZs9FAZzfHomQYuCRD8/M0Q3frSo6BQTI0P8mwcJDPRPOTDM1PMjQ/+Uw0P8nQ/PKUS449aJVSPwIdgbJKqUPAGxhjlqK1ngjMxhgANwpjXIwH07x8DVBbKVXDVZg+QL+c34vwpr59+7J48WJOnjwJ0FgpNRBIBF5TSv0EnAIuAsuRDANWxhwx8npNKRUGbAQOA9vJfCxKhgFCMjS/fGQIkmNAkAzNTzIsHJJzBOyu7xjTkM9EU5F6jflJhuYnn4nmJxkWSjOBJ13tbW1I36ieNV3ws5v1AHZhzM72muu5ES1atNDC94D9xv9QwCeuXP4DWmrJ0DSAtZKhuXkrQ+BmydE/cpuhlmMx4EiG5icZmp/UTQsHqZuan9RNzU8+E81PMjS/vGSY/M+jScIKgoyH4R9KqXVa65be2JZk6D/eylEy9B85Fs1PMjQ/ydD8JEPzkwwLB6mbmp8ci+YnGZqfZGh++cnQG2PQCiGEEEIIIYQQQgghhMgDaaAVQgghhBBCCCGEEEIIP5EGWiGEEEIIIYQQQgghhPATaaAVQgghhBBCCCGEEEIIP5EGWiGEEEIIIYQQQgghhPATaaAVQgghhBBCCCGEEEIIP5EGWiGEEEIIIYQQQgghhPATaaAVQgghhBBCCCGEEEIIP5EGWiGEEEIIIYQQQgghhPATaaAVQgghhBBCCCGEEEIIP5EGWiGEEEIIIYQQQgghhPATaaAVQgghhBBCCCGEEEIIP5EGWiGEEEIIIYQQQgghhPATaaAVQgghhBBCCCGEEEIIP5EGWiGEEEIIIYQQQgghhPATjxpolVLdlFI7lVJRSqkhbpb3V0qdUEptdP17yPtFFfkxd+5c6tatC9BIMjSn5AwjIyMBKmZcLhkGPsmwcJDzqflJhuYnGZqfZGh+Uq8xP8mwcJDzqflJhgI8aKBVSlmBT4DuQAOgr1KqgZtVf9ZaN3X9+9LL5RT54HA4eOKJJ5gzZw7AViRD00mb4bZt2wBKS4bmIhkWDnI+NT/J0PwkQ/OTDM1P6jXmJxkWDnI+NT/JUCTzpAdtayBKa71Xa50A/AT0LthiCW9avXo1kZGR1KxZE0AjGZpO2gyDg4MBTiMZmopkWDjI+dT8JEPzkwzNTzI0P6nXmJ9kWDjI+dT8JEORzJMG2nDgYJrHh1zPZXS7UmqzUmqaUqqquw0ppQYppdYqpdaeOHEiD8UVeRETE0PVqukikQxNxk2GCUiGpuLNDEFy9Bc5n5qfZGh+kqH5SYbmJ3VT85O6aeEg51PzkwxFMm9NEvYHEKG1bgzMB75xt5LW+gutdUutdcty5cp5adfCSyRD85MMzc+jDEFyDHByLJqfZGh+kqH5SYbmJxman9RNCwc5Fs1PMrwCeNJAGwOkbZ2v4nouhdb6lNY63vXwS6CFd4onvCE8PJyDB9N2gpYMzcZNhsFIhqYiGRYOcj41P8nQ/CRD85MMzU/qNeYnGRYOcj41P8lQJPOkgXYNUFspVUMpFQz0AWamXUEpVSnNw5uB7d4rosivVq1asXv3bvbt2wegkAxNJ22GCQkJAKWRDE1FMiwc5HxqfpKh+UmG5icZmp/Ua8xPMiwc5HxqfpKhSGbLaQWtdZJS6klgHmAFvtJab1VKjQDWaq1nAk8rpW4GkjAGF+9fgGUWuWSz2ZgwYQJdu3YFaAiMlAzNJW2GDocD4LRkaC6SYeEg51PzkwzNTzI0P8nQ/KReY36SYeEg51PzkwxFMqW19suOW7ZsqdeuXeuXfV/JlFLrtNYtvbEtydB/vJWjZOg/ciyan2RofpKh+UmG5icZFg5SNzU/ORbNTzI0P8nQ/PKTobcmCRNCCCGEEEIIIYQQQgiRS9JAK4QQQgghhBBCCCGEEH4iDbRCCCGEEEIIIYQQQgjhJ9JAK4QQQgghhBBCCCGEEH4iDbRCCCGEEEIIIYQQQgjhJ9JAK4QQQgghhBBCCCGEEH4iDbRCCCGEEEIIIYQQQgjhJ9JAK4QQQgghhBBCCCGEEH4iDbRCCCGEEEIIIYQQQgjhJ9JAK4QQQgghhBBCCCGEEH4iDbRCCCGEEEIIIYQQQgjhJ9JAK4QQQgghhBBCCCGEEH4iDbRCCCGEEEIIIYQQQgjhJ9JAK4QQQgghhBBCCCGEEH4iDbRCCCGEEEIIIYQQQgjhJx410CqluimldiqlopRSQ9wstyulfnYtX6WUivB6SUW+zJ07l7p16wI0kgzNKTnDyMhIgIoZl0uGgU8yLBzkfGp+kqH5SYbmJxman9RrzE8yLBzkfGp+kqEADxpolVJW4BOgO9AA6KuUapBhtYHAGa11JPAB8La3CyryzuFw8MQTTzBnzhyArUiGppM2w23btgGUlgzNRTIsHOR8an6SoflJhuYnGZqf1GvMTzIsHOR8an6SoUjmSQ/a1kCU1nqv1joB+AnonWGd3sA3rp+nATcopZT3iinyY/Xq1URGRlKzZk0AjWRoOmkzDA4OBjiNZGgqkmHhIOdT85MMzU8yND/J0PykXmN+kmHhIOdT85MMRTJPGmjDgYNpHh9yPed2Ha11EnAOKOONAor8i4mJoWrVqmmfkgxNxk2GCUiGpiIZFg5yPjU/ydD8JEPzkwzNT+o15icZFg5yPjU/yVAks/lyZ0qpQcAg18N4pdQWX+7fi8oCJ/1diFwoBZSYNGnSfqBufjYkGfpN2gwBGuZ1Q4UoQzBXjl7LEApVjmbKEOR86o5kaJAMfUcyzEwyNEiGviN1U/fMlKPUTd0zU4Yg51N3JEODZOgfec7QkwbaGCBtc34V13Pu1jmklLIBYcCpjBvSWn8BfAGglFqrtW6Zl0L7m9nKrpS6Ghiute6qlFqLZGi6sqfN0PX4EFd4hmCu8nszQyg8OZqt7HI+zcxsZZcMMzNb2SXDzMxWdskwM7OVXeqm7pmp/FI3dc9sZZfzaWZmK7tkmJnZy57X13oyxMEaoLZSqoZSKhjoA8zMsM5M4AHXz3cAC7XWOq+FEl6XkiGgkAzNKONxWBrJ0Gwkw8JBzqfmJxman2RofpKh+Um9xvwkw8JBzqfmJxkKwIMGWtf4Fk8C84DtwFSt9Val1Ail1M2u1SYBZZRSUcBzwJCCKrDIvQwZNkQyNB03x+FpydBcJMPCQc6n5icZmp9kaH6SoflJvcb8JMPCQc6n5icZimTKX43uSqlBru7XpiNl9/62fM3MZQfvlV9+D/4jx6JByu79bfmalN372/I1Kbv3t+VrUnbvb8vXzFx2kLppMjOXX45Fg5Td+9vyNSm797fla1dq2f3WQCuEEEIIIYQQQgghhBBXuhyHOFBKfaWUOq6ymP1NGcYrpaKUUpuVUs29X0yRH5Jh4SA5mp9kaH6SoflJhuYnGRYOkqP5SYbmJxman2RofpKhSObJJGGTgW7ZLO8O1Hb9GwR8lnahUqqbUmqn648p0zgZSim7Uupn1/JVSqkIj0tfwDwoe3+l1Aml1EbXv4f8UU53Mhzkk8mQYdqDHNgLtEAyDOQMIUOOGU/UGOPWuD0WJUP/kAwNkmG67UmOPiYZppIMU9aXDP1APhMNkmHKupKhH0iGBskw3fYkRx+TDFMVogwzLk+XofK0UV1rneM/IALYksWyz4G+aR7vBCq5frYCe4CaQDCwCWiQ4fWPAxNdP/cBfvakTAX9z8Oy9wcm+LusWZS/A9A8ObeMGQI9gDmAAn4DoiTDwPqXMcOMOWbIsC1wzN2xKBlKhpKh/zPMxe9CcpQMJUPJsNBl6M0cJUPJUDKUDCVD+Uw0c46SYeHMMMPyjBmu8mS7nvSgzUk4cDDN40Ou5wBaYzT87dVaJwA/Ab0zvL438I3r52nADUop5YVy5ZcnZQ9YWuulwOlsVukNfKuNv54gIFgpVcm1TDIMALnJUGu9EggFLqVZnpyjZOgnkiEgGcr51M8kwxSSoUEy9BP5TAQkQ8nQzyRDQDKUz0Q/kwxTXEkZlkzT3pYljyYJc3WB/lNr3cjNsj+BsVrr5a7HC4CXtdZrlVJ3AN201g+5lk0CegEHQkNDW9SrVy/HfQvviI+PJyoqiri4uJNa63Jpc3P9XBF41JWbZBig0uYIrCL9sXcSI7dJrscLgJcxrsZJhgHCSxneBwwAigNIjr6VlwzlfBpYJEPzkwwLh/j4eLZs2eLQWtvcfKeQeo0JSN3U/KRuan7ymWh+kmHhsW7dOncZpmSW3WttXth/DFA1zeMqrufcWQxc1lo/2bJlS712bbZlE14UHR1Nz5492bp16343i2MwPmSTSYYBKocc44EKaR4n5xiRYb3FSIZ+46UMAbZqrZ8EkBx9K48ZurMYORb9QjI0P8mwcIiOjqZGjRqJWSyWeo0JSN3U/KRuan7ymWh+kmHhoZRyl6FHvDHEwUzgftcguG2Bc1rrI65luWm8Fb6VNpuZGB+yMZKhqWTMRgOd3RyLkmHgkgzNz9MM3a0rOQYGydD8JMPCQT4TzU8yND/J0PzkM9H8JEPzy1MuOfagVUr9CHQEyiqlDgFvYIxZitZ6IjAbYwDcKIxxMR5M8/I1QG2lVA1XYfoA/XJ+L8Kb+vbty+LFizl58iRAY6XUQCAReE0p9RNwCrgILOf/7N1RjJTnfej/709s1/8jq0lLjGoLUwFaCwtbVhTWTqJzVEVKJXDqgtTkwrRqS2sLVQXlojdxlYtGzk1zFZ0Kq4mPsXDPRUxkNSqxAlbkCkW+qPFyjuuyWNRbGxc2aQMmsk6beolXz/9i3sHD7ADD7rvzzu/l+5FG3pl39p3HfP0+8/BkM2vDsdXfkU6vr0bEx4HXgR8Db7L0WrThmLBhfitoCHYcCzbMz4bt0O0I3Fb9HeMFfE9MxXVNfjbMz/fE/GzYSkeA/dV+26e5elP92srq/3azLwD/TOe3s321euzJbdu2FY0e8G7nHwTwVNXln4DpYsM0gBkb5lZXQ2CnHZtxsw2L1+LYsWF+NszPtWk7uDbNz7Vpfr4n5mfD/JbTsHsb6peErQY/D6MZEXGylDJdx7ls2Jy6OtqwOV6L+dkwPxvmZ8P8bNgOrk3z81rMz4b52TC/lTSs4zNoJUmSJEmSJEnL4AatJEmSJEmSJDXEDVpJkiRJkiRJaogbtJIkSZIkSZLUEDdoJUmSJEmSJKkhbtBKkiRJkiRJUkPcoJUkSZIkSZKkhrhBK0mSJEmSJEkNcYNWkiRJkiRJkhriBq0kSZIkSZIkNcQNWkmSJEmSJElqiBu0kiRJkiRJktQQN2glSZIkSZIkqSFu0EqSJEmSJElSQ4baoI2IHRFxJiLmIuKJAcf3RMSFiHi9uj1e/1C1EseOHWPLli0A99swp27DqakpgDv7j9tw/NmwHZxP87NhfjbMz4b5ua7Jz4bt4Hyanw0FQ2zQRsQa4CngYWArsDsitg546uFSyier2zM1j1MrsLi4yL59+zh69CjALDZMp7fh6dOnAdbaMBcbtoPzaX42zM+G+dkwP9c1+dmwHZxP87Ohuob5CdqHgLlSytullMvA88Cu1R2W6nTixAmmpqbYvHkzQMGG6fQ2nJycBLiEDVOxYTs4n+Znw/xsmJ8N83Ndk58N28H5ND8bqmuYDdr1wLme++erx/p9MSLeiIgXImJDLaNTLebn59mw4aokNkxmQMPL2DAVG7aD82l+NszPhvnZMD/XNfnZsB2cT/Ozobrq+iVh3wc2llIeAH4IPDfoSRGxNyJmImLmwoULNb20amLD/GyY31ANwY5jzmsxPxvmZ8P8bJifDfNzbdoOXov52fAWMMwG7TzQuzt/d/XYFaWU90opC9XdZ4Btg05USnm6lDJdSplet27dcsarZVi/fj3nzvX+ELQNsxnQcBIbplJnw+q5dmyA82l+NszPhvnZMD/Xpvm5Nm0H59P8bKiuYTZoXwPuiYhNETEJPAoc6X1CRNzVc3cn8GZ9Q9RKPfjgg7z11lu88847AIEN0+ltePnyZYC12DAVG7aD82l+NszPhvnZMD/XNfnZsB2cT/OzobombvSEUsqHEbEfeAlYAzxbSpmNiCeBmVLKEeDLEbET+JDOh4vvWcUx6yZNTExw4MABtm/fDnAf8HUb5tLbcHFxEeCSDXOxYTs4n+Znw/xsmJ8N83Ndk58N28H5ND8bqitKKY288PT0dJmZmWnktW9lEXGylDJdx7ls2Jy6OtqwOV6L+dkwPxvmZ8P8bNgOrk3z81rMz4b52TC/lTSs65eESZIkSZIkSZJukhu0kiRJkiRJktQQN2glSZIkSZIkqSFu0EqSJEmSJElSQ9yglSRJkiRJkqSGuEErSZIkSZIkSQ1xg1aSJEmSJEmSGuIGrSRJkiRJkiQ1xA1aSZIkSZIkSWqIG7SSJEmSJEmS1BA3aCVJkiRJkiSpIW7QSpIkSZIkSVJD3KCVJEmSJEmSpIa4QStJkiRJkiRJDXGDVpIkSZIkSZIaMtQGbUTsiIgzETEXEU8MOH5bRByujr8aERtrH6lW5NixY2zZsgXgfhvm1G04NTUFcGf/cRuOPxu2g/NpfjbMz4b52TA/1zX52bAdnE/zs6FgiA3aiFgDPAU8DGwFdkfE1r6nPQb8rJQyBXwT+EbdA9XyLS4usm/fPo4ePQowiw3T6W14+vRpgLU2zMWG7eB8mp8N87NhfjbMz3VNfjZsB+fT/GyormF+gvYhYK6U8nYp5TLwPLCr7zm7gOeqr18APh8RUd8wtRInTpxgamqKzZs3AxRsmE5vw8nJSYBL2DAVG7aD82l+NszPhvnZMD/XNfnZsB2cT/OzobqG2aBdD5zruX++emzgc0opHwLvA5+oY4Baufn5eTZs2ND7kA2TGdDwMjZMxYbt4Hyanw3zs2F+NszPdU1+NmwH59P8bKiuiVG+WETsBfZWdxci4tQoX79GdwAXmx7ETfhV4GMHDx58F9iykhPZsDG9DQHuW+6JWtQQcnWsrSG0qmOmhuB8OogNO2w4OjZcyoYdNhwd16aDZero2nSwTA3B+XQQG3bYsBnLbjjMBu080Ludf3f12KDnnI+ICeDjwHv9JyqlPA08DRARM6WU6eUMumnZxh4RnwW+VkrZHhEz2DDd2HsbVvfPc4s3hFzjr7MhtKdjtrE7ny6Vbew2XCrb2G24VLax23CpbGN3bTpYpvG7Nh0s29idT5fKNnYbLpV97Mv93mE+4uA14J6I2BQRk8CjwJG+5xwB/rD6+kvA35dSynIHpdpdaQgENsyo/zpciw2zsWE7OJ/mZ8P8bJifDfNzXZOfDdvB+TQ/GwoYYoO2+nyL/cBLwJvAd0spsxHxZETsrJ52EPhERMwBfwY8sVoD1s3ra3gfNkxnwHV4yYa52LAdnE/zs2F+NszPhvm5rsnPhu3gfJqfDdUVTW26R8Te6sev03Hs9Z9r1DKPHeobv38OzfFa7HDs9Z9r1Bx7/ecaNcde/7lGzbHXf65Ryzx2cG3alXn8Xosdjr3+c42aY6//XKN2q469sQ1aSZIkSZIkSbrV3fAjDiLi2Yj4aVzjt79Fx19FxFxEvBERn6p/mFoJG7aDHfOzYX42zM+G+dmwHeyYnw3zs2F+NszPhuoa5peEHQJ2XOf4w8A91W0v8Ne9ByNiR0Scqf5jWvI5GRFxW0Qcro6/GhEbhx79Khti7Hsi4kJEvF7dHm9inIP0XeSH6GvYe5EDbwPbsOE4N4S+jv0TNZ3PrRl4LdqwGTbssOFV57PjiNnwIza88nwbNsD3xA4bXnmuDRtgww4bXnU+O46YDT/Soob9x69qGMNuqpdSbngDNgKnrnHs28DunvtngLuqr9cA/wJsBiaBfwS29n3/nwLfqr5+FDg8zJhW+zbk2PcAB5oe6zXG/xvAp7rd+hsCXwCOAgF8D5iz4Xjd+hv2d+xr+Bng3wddiza0oQ2bb3gTfxZ2tKENbdi6hnV2tKENbWhDG/qemLmjDdvZsO94f8NXhznvMD9BeyPrgXM9989XjwE8RGfj7+1SymXgeWBX3/fvAp6rvn4B+HxERA3jWqlhxj62Sik/Ai5d5ym7gL8pnf96fgmYjIi7qmM2HAM307CU8g/A7cDPe453O9qwITYEbOh82jAbXmHDDhs2xPdEwIY2bJgNARv6ntgwG15xKzX8lZ79tmsa6peEVT8C/WIp5f4Bx14E/rKU8kp1/2XgK6WUmYj4ErCjlPJ4dewg8NvAv95+++3b7r333hu+tuqxsLDA3NwcH3zwwcVSyrrebtXXdwJ/UnWz4Zjq7Qi8ytXX3kU63Q5W918GvkLnf42z4ZioqeHvA38M/DKAHUdrOQ2dT8eLDfOzYTssLCxw6tSpxVLKxIC/U7iuScC1aX6uTfPzPTE/G7bHyZMnBzW80ux63ztRw+vPAxt67t9dPTbIceC/Sin7p6eny8zMdcemGp09e5ZHHnmE2dnZdwccnqfzJttlwzF1g44LwK/13O923Nj3vOPYsDE1NQSYLaXsB7DjaC2z4SDH8VpshA3zs2E7nD17lk2bNv3iGodd1yTg2jQ/16b5+Z6Ynw3bIyIGNRxKHR9xcAT4g+pDcD8DvF9K+Ul17GY2bzVavW2O0HmTnbdhKv1tCvCbA65FG44vG+Y3bMNBz7XjeLBhfjZsB98T87NhfjbMz/fE/GyY37K63PAnaCPiO8DngDsi4jzwF3Q+s5RSyreAH9D5ANw5Op+L8Uc93/4acE9EbKoG8yjwuzf+d1Gddu/ezfHjx7l48SLAAxHxGPAL4KsR8TzwHvCfwCvYcGz1d6TT66sR8XHgdeDHwJssvRZtOCZsmN8KGoIdx4IN87NhO3Q7ArdVf8d4Ad8TU3Fdk58N8/M9MT8bttIRYH+13/Zprt5Uv7ay+r/d7AvAP9P57WxfrR57ctu2bUWjB7zb+QcBPFV1+SdgutgwDWDGhrnV1RDYacdm3GzD4rU4dmyYnw3zc23aDq5N83Ntmp/vifnZML/lNOzehvolYavBz8NoRkScLKVM13EuGzanro42bI7XYn42zM+G+dkwPxu2g2vT/LwW87NhfjbMbyUN6/gMWkmSJEmSJEnSMrhBK0mSJEmSJEkNcYNWkiRJkiRJkhriBq0kSZIkSZIkNcQNWkmSJEmSJElqiBu0kiRJkiRJktQQN2glSZIkSZIkqSFu0EqSJEmSJElSQ9yglSRJkiRJkqSGuEErSZIkSZIkSQ1xg1aSJEmSJEmSGuIGrSRJkiRJkiQ1xA1aSZIkSZIkSWqIG7SSJEmSJEmS1JChNmgjYkdEnImIuYh4YsDxPRFxISJer26P1z9UrcSxY8fYsmULwP02zKnbcGpqCuDO/uM2HH82bAfn0/xsmJ8N87Nhfq5r8rNhOzif5mdDwRAbtBGxBngKeBjYCuyOiK0Dnnq4lPLJ6vZMzePUCiwuLrJv3z6OHj0KMIsN0+ltePr0aYC1NszFhu3gfJqfDfOzYX42zM91TX42bAfn0/xsqK5hfoL2IWCulPJ2KeUy8Dywa3WHpTqdOHGCqakpNm/eDFCwYTq9DScnJwEuYcNUbNgOzqf52TA/G+Znw/xc1+Rnw3ZwPs3PhuoaZoN2PXCu5/756rF+X4yINyLihYjYUMvoVIv5+Xk2bLgqiQ2TGdDwMjZMxYbt4Hyanw3zs2F+NszPdU1+NmwH59P8bKiuun5J2PeBjaWUB4AfAs8NelJE7I2ImYiYuXDhQk0vrZrYMD8b5jdUQ7DjmPNazM+G+dkwPxvmZ8P8XJu2g9difja8BQyzQTsP9O7O3109dkUp5b1SykJ19xlg26ATlVKeLqVMl1Km161bt5zxahnWr1/PuXO9PwRtw2wGNJzEhqnU2bB6rh0b4Hyanw3zs2F+NszPtWl+rk3bwfk0Pxuqa5gN2teAeyJiU0RMAo8CR3qfEBF39dzdCbxZ3xC1Ug8++CBvvfUW77zzDkBgw3R6G16+fBlgLTZMxYbt4Hyanw3zs2F+NszPdU1+NmwH59P8bKiuiRs9oZTyYUTsB14C1gDPllJmI+JJYKaUcgT4ckTsBD6k8+Hie1ZxzLpJExMTHDhwgO3btwPcB3zdhrn0NlxcXAS4ZMNcbNgOzqf52TA/G+Znw/xc1+Rnw3ZwPs3PhuqKUkojLzw9PV1mZmYaee1bWUScLKVM13EuGzanro42bI7XYn42zM+G+dkwPxu2g2vT/LwW87NhfjbMbyUN6/olYZIkSZIkSZKkm+QGrSRJkiRJkiQ1xA1aSZIkSZIkSWqIG7SSJEmSJEmS1BA3aCVJkiRJkiSpIW7QSpIkSZIkSVJD3KCVJEmSJEmSpIa4QStJkiRJkiRJDXGDVpIkSZIkSZIa4gatJEmSJEmSJDXEDVpJkiRJkiRJaogbtJIkSZIkSZLUEDdoJUmSJEmSJKkhbtBKkiRJkiRJUkPcoJUkSZIkSZKkhrhBK0mSJEmSJEkNGWqDNiJ2RMSZiJiLiCcGHL8tIg5Xx1+NiI21j1QrcuzYMbZs2QJwvw1z6jacmpoCuLP/uA3Hnw3bwfk0PxvmZ8P8bJif65r8bNgOzqf52VAwxAZtRKwBngIeBrYCuyNia9/THgN+VkqZAr4JfKPugWr5FhcX2bdvH0ePHgWYxYbp9DY8ffo0wFob5mLDdnA+zc+G+dkwPxvm57omPxu2g/NpfjZU1zA/QfsQMFdKebuUchl4HtjV95xdwHPV1y8An4+IqG+YWokTJ04wNTXF5s2bAQo2TKe34eTkJMAlbJiKDdvB+TQ/G+Znw/xsmJ/rmvxs2A7Op/nZUF3DbNCuB8713D9fPTbwOaWUD4H3gU/UMUCt3Pz8PBs2bOh9yIbJDGh4GRumYsN2cD7Nz4b52TA/G+bnuiY/G7aD82l+NlTXxChfLCL2AnuruwsRcWqUr1+jO4CLTQ/iJvwq8LGDBw++C2xZyYls2JjehgD3LfdELWoIuTrW1hBa1TFTQ3A+HcSGHTYcHRsuZcMOG46Oa9PBMnV0bTpYpobgfDqIDTts2IxlNxxmg3Ye6N3Ov7t6bNBzzkfEBPBx4L3+E5VSngaeBoiImVLK9HIG3bRsY4+IzwJfK6Vsj4gZbJhu7L0Nq/vnucUbQq7x19kQ2tMx29idT5fKNnYbLpVt7DZcKtvYbbhUtrG7Nh0s0/hdmw6WbezOp0tlG7sNl8o+9uV+7zAfcfAacE9EbIqISeBR4Ejfc44Af1h9/SXg70spZbmDUu2uNAQCG2bUfx2uxYbZ2LAdnE/zs2F+NszPhvm5rsnPhu3gfJqfDQUMsUFbfb7FfuAl4E3gu6WU2Yh4MiJ2Vk87CHwiIuaAPwOeWK0B6+b1NbwPG6Yz4Dq8ZMNcbNgOzqf52TA/G+Znw/xc1+Rnw3ZwPs3PhuqKpjbdI2Jv9ePX6Tj2+s81apnHDvWN3z+H5ngtdjj2+s81ao69/nONmmOv/1yj5tjrP9eoZR47uDbtyjx+r8UOx17/uUbNsdd/rlG7Vcfe2AatJEmSJEmSJN3qbvgRBxHxbET8NK7x29+i468iYi4i3oiIT9U/TK2EDdvBjvnZMD8b5mfD/GzYDnbMz4b52TA/G+ZnQ3UN80vCDgE7rnP8YeCe6rYX+OvegxGxIyLOVP8xLfmcjIi4LSIOV8dfjYiNQ49+lQ0x9j0RcSEiXq9ujzcxzkH6LvJD9DXsvciBt4Ft2HCcG0Jfx/6Jms7n1gy8Fm3YDBt22PCq89lxxGz4ERteeb4NG+B7YocNrzzXhg2wYYcNrzqfHUfMhh9pUcP+41c1jGE31UspN7wBG4FT1zj2bWB3z/0zwF3V12uAfwE2A5PAPwJb+77/T4FvVV8/ChweZkyrfRty7HuAA02P9Rrj/w3gU91u/Q2BLwBHgQC+B8zZcLxu/Q37O/Y1/Azw74OuRRva0IbNN7yJPws72tCGNmxdwzo72tCGNrShDX1PzNzRhu1s2He8v+Grw5x3qM+grXbYXyyl3D/g2IvAX5ZSXqnuvwx8pZQyExGfBb5WStleHftb4CHg326//fZt99577w1fW/VYWFhgbm6ODz744GIpZV1EfBs4Xkr5TtXwAeDTpZSf2HB89XYE/paqIUBE/Afwe6WUv6vuvwx8BfglbDg2amr458CDwK8D2HG0ltPQ+XS82DA/G7bDwsICp06dWiylTPSuTcF1TRauTfNzbZqf74n52bA9Tp48OajhGeBzpZSfXO97J1Z5bOuBcz33vwf8uJSyf3p6uszMzKzyy6vr7NmzPPLII8zOzr5bPdTf5qfVY/3/wdhwjPR17G/zAXDHgG+z4RipqeF5YH0p5XcA7Dhay2zIgOd6LTbEhvnZsB3Onj3Lpk2bflHddV2TkGvT/Fyb5ud7Yn42bI+IGNTwPIP3264yzGfQ3sg8sKHn/t3VY8pjHvj/eu7bMKcF4Nd67tsxHxvmZ8P8bJifDdvBjvnZMD8b5mfD/Gx4i6hjg/YI8AfVh+B+Bni/58d23bwdX71tjtD5zJN5G6bS36YAvzngWrTh+LJhfsM2HPRcO44HG+Znw3bwPTE/G+Znw/x8T8zPhvktq8sNP+IgIr4DfA64IyLOA39B5zNnKKV8C/gBnQ/AnQN+DvxRz7e/BtwTEZuqwTwK/O6N/11Up927d3P8+HEuXrwI8EBEPAb8AvhqRDwPvAf8J/AKNhxb/R3p9PpqRHwceB34MfAmS69FG44JG+a3goZgx7Fgw/xs2A7djsBt1d8xXsD3xFRc1+Rnw/x8T8zPhq10BNhf7bd9mqs31a9tBL/d7AvAP9P57WxfrR57ctu2bUWjB7zb+QcBPFV1+SdgutgwDWDGhrnV1RDYacdm3GzD4rU4dmyYnw3zc23aDq5N83Ntmp/vifnZML/lNOzeovP9o+cHFjcjIk6WUqbrOJcNm1NXRxs2x2sxPxvmZ8P8bJifDdvBtWl+Xov52TA/G+a3koZ1fAatJEmSJEmSJGkZ3KCVJEmSJEmSpIa4QStJkiRJkiRJDXGDVpIkSZIkSZIa4gatJEmSJEmSJDXEDVpJkiRJkiRJaogbtJIkSZIkSZLUEDdoJUmSJEmSJKkhbtBKkiRJkiRJUkPcoJUkSZIkSZKkhrhBK0mSJEmSJEkNcYNWkiRJkiRJkhriBq0kSZIkSZIkNcQNWkmSJEmSJElqyFAbtBGxIyLORMRcRDwx4PieiLgQEa9Xt8frH6pW4tixY2zZsgXgfhvm1G04NTUFcGf/cRuOPxu2g/NpfjbMz4b52TA/1zX52bAdnE/zs6FgiA3aiFgDPAU8DGwFdkfE1gFPPVxK+WR1e6bmcWoFFhcX2bdvH0ePHgWYxYbp9DY8ffo0wFob5mLDdnA+zc+G+dkwPxvm57omPxu2g/NpfjZU1zA/QfsQMFdKebuUchl4Hti1usNSnU6cOMHU1BSbN28GKNgwnd6Gk5OTAJewYSo2bAfn0/xsmJ8N87Nhfq5r8rNhOzif5mdDdQ2zQbseONdz/3z1WL8vRsQbEfFCRGyoZXSqxfz8PBs2XJXEhskMaHgZG6Ziw3ZwPs3PhvnZMD8b5ue6Jj8btoPzaX42VFddvyTs+8DGUsoDwA+B5wY9KSL2RsRMRMxcuHChppdWTWyYnw3zG6oh2HHMeS3mZ8P8bJifDfOzYX6uTdvBazE/G94ChtmgnQd6d+fvrh67opTyXillobr7DLBt0IlKKU+XUqZLKdPr1q1bzni1DOvXr+fcud4fgrZhNgMaTmLDVOpsWD3Xjg1wPs3PhvnZMD8b5ufaND/Xpu3gfJqfDdU1zAbta8A9EbEpIiaBR4EjvU+IiLt67u4E3qxviFqpBx98kLfeeot33nkHILBhOr0NL1++DLAWG6Ziw3ZwPs3PhvnZMD8b5ue6Jj8btoPzaX42VNfEjZ5QSvkwIvYDLwFrgGdLKbMR8SQwU0o5Anw5InYCH9L5cPE9qzhm3aSJiQkOHDjA9u3bAe4Dvm7DXHobLi4uAlyyYS42bAfn0/xsmJ8N87Nhfq5r8rNhOzif5mdDdUUppZEXnp6eLjMzM4289q0sIk6WUqbrOJcNm1NXRxs2x2sxPxvmZ8P8bJifDdvBtWl+Xov52TA/G+a3koZ1/ZIwSZIkSZIkSdJNcoNWkiRJkiRJkhriBq0kSZIkSZIkNcQNWkmSJEmSJElqiBu0kiRJkiRJktQQN2glSZIkSZIkqSFu0EqSJEmSJElSQ9yglSRJkiRJkqSGuEErSZIkSZIkSQ1xg1aSJEmSJEmSGuIGrSRJkiRJkiQ1xA1aSZIkSZIkSWqIG7SSJEmSJEmS1BA3aCVJkiRJkiSpIW7QSpIkSZIkSVJDhtqgjYgdEXEmIuYi4okBx2+LiMPV8VcjYmPtI9WKHDt2jC1btgDcb8Ocug2npqYA7uw/bsPxZ8N2cD7Nz4b52TA/G+bnuiY/G7aD82l+NhQMsUEbEWuAp4CHga3A7ojY2ve0x4CflVKmgG8C36h7oFq+xcVF9u3bx9GjRwFmsWE6vQ1Pnz4NsNaGudiwHZxP87NhfjbMz4b5ua7Jz4bt4Hyanw3VNcxP0D4EzJVS3i6lXAaeB3b1PWcX8Fz19QvA5yMi6humVuLEiRNMTU2xefNmgIIN0+ltODk5CXAJG6Ziw3ZwPs3PhvnZMD8b5ue6Jj8btoPzaX42VNcwG7TrgXM9989Xjw18TinlQ+B94BN1DFArNz8/z4YNG3ofsmEyAxpexoap2LAdnE/zs2F+NszPhvm5rsnPhu3gfJqfDdU1McoXi4i9wN7q7kJEnBrl69foDuBi04O4Cb8KfOzgwYPvAltWciIbNqa3IcB9yz1RixpCro61NYRWdczUEJxPB7Fhhw1Hx4ZL2bDDhqPj2nSwTB1dmw6WqSE4nw5iww4bNmPZDYfZoJ0Herfz764eG/Sc8xExAXwceK//RKWUp4GnASJippQyvZxBNy3b2CPis8DXSinbI2IGG6Ybe2/D6v55bvGGkGv8dTaE9nTMNnbn06Wyjd2GS2Ubuw2XyjZ2Gy6VbeyuTQfLNH7XpoNlG7vz6VLZxm7DpbKPfbnfO8xHHLwG3BMRmyJiEngUONL3nCPAH1Zffwn4+1JKWe6gVLsrDYHAhhn1X4drsWE2NmwH59P8bJifDfOzYX6ua/KzYTs4n+ZnQwFDbNBWn2+xH3gJeBP4billNiKejIid1dMOAp+IiDngz4AnVmvAunl9De/DhukMuA4v2TAXG7aD82l+NszPhvnZMD/XNfnZsB2cT/OzobqiqU33iNhb/fh1Oo69/nONWuaxQ33j98+hOV6LHY69/nONmmOv/1yj5tjrP9eoOfb6zzVqmccOrk27Mo/fa7HDsdd/rlFz7PWfa9Ru1bE3tkErSZIkSZIkSbe6G37EQUQ8GxE/jWv89rfo+KuImIuINyLiU/UPUythw3awY342zM+G+dkwPxu2gx3zs2F+NszPhvnZUF3D/JKwQ8CO6xx/GLinuu0F/rr3YETsiIgz1X9MSz4nIyJui4jD1fFXI2Lj0KNfZUOMfU9EXIiI16vb402Mc5C+i/wQfQ17L3LgbWAbNhznhtDXsX+ipvO5NQOvRRs2w4YdNrzqfHYcMRt+xIZXnm/DBvie2GHDK8+1YQNs2GHDq85nxxGz4Uda1LD/+FUNY9hN9VLKDW/ARuDUNY59G9jdc/8McFf19RrgX4DNwCTwj8DWvu//U+Bb1dePAoeHGdNq34Yc+x7gQNNjvcb4fwP4VLdbf0PgC8BRIIDvAXM2HK9bf8P+jn0NPwP8+6Br0YY2tGHzDW/iz8KONrShDVvXsM6ONrShDW1oQ98TM3e0YTsb9h3vb/jqMOcd5idob2Q9cK7n/vnqMYCH6Gz8vV1KuQw8D+zq+/5dwHPV1y8An4+IqGFcKzXM2MdWKeVHwKXrPGUX8Del81/PLwGTEXFXdcyGY+BmGpZS/gG4Hfh5z/FuRxs2xIaADZ1PG2bDK2zYYcOG+J4I2NCGDbMhYEPfExtmwytupYa/0rPfdk1D/ZKw6kegXyyl3D/g2IvAX5ZSXqnuvwx8pZQyExFfAnaUUh6vjh0Efhv419tvv33bvffee8PXVj0WFhaYm5vjgw8+uFhKWdfbrfr6TuBPqm42HFO9HYFXufrau0in28Hq/svAV+j8r3E2HBM1Nfx94I+BXwaw42gtp6Hz6XixYX42bIeFhQVOnTq1WEqZGPB3Ctc1Cbg2zc+1aX6+J+Znw/Y4efLkoIZXml3veydqeP15YEPP/burxwY5DvxXKWX/9PR0mZm57thUo7Nnz/LII48wOzv77oDD83TeZLtsOKZu0HEB+LWe+92OG/uedxwbNqamhgCzpZT9AHYcrWU2HOQ4XouNsGF+NmyHs2fPsmnTpl9c47DrmgRcm+bn2jQ/3xPzs2F7RMSghkOp4yMOjgB/UH0I7meA90spP6mO3czmrUart80ROm+y8zZMpb9NAX5zwLVow/Flw/yGbTjouXYcDzbMz4bt4HtifjbMz4b5+Z6Ynw3zW1aXG/4EbUR8B/gccEdEnAf+gs5nllJK+RbwAzofgDtH53Mx/qjn218D7omITdVgHgV+98b/LqrT7t27OX78OBcvXgR4ICIeA34BfDUingfeA/4TeAUbjq3+jnR6fTUiPg68DvwYeJOl16INx4QN81tBQ7DjWLBhfjZsh25H4Lbq7xgv4HtiKq5r8rNhfr4n5mfDVjoC7K/22z7N1Zvq11ZW/7ebfQH4Zzq/ne2r1WNPbtu2rWj0gHc7/yCAp6ou/wRMFxumAczYMLe6GgI77diMm21YvBbHjg3zs2F+rk3bwbVpfq5N8/M9MT8b5recht3bUL8kbDX4eRjNiIiTpZTpOs5lw+bU1dGGzfFazM+G+dkwPxvmZ8N2cG2an9difjbMz4b5raRhHZ9BK0mSJEmSJElaBjdoJUmSJEmSJKkhbtBKkiRJkiRJUkPcoJUkSZIkSZKkhrhBK0mSJEmSJEkNcYNWkiRJkiRJkhriBq0kSZIkSZIkNcQNWkmSJEmSJElqiBu0kiRJkiRJktQQN2glSZIkSZIkqSFu0EqSJEmSJElSQ9yglSRJkiRJkqSGuEErSZIkSZIkSQ1xg1aSJEmSJEmSGjLUBm1E7IiIMxExFxFPDDi+JyIuRMTr1e3x+oeqlTh27BhbtmwBuN+GOXUbTk1NAdzZf9yG48+G7eB8mp8N87NhfjbMz3VNfjZsB+fT/GwoGGKDNiLWAE8BDwNbgd0RsXXAUw+XUj5Z3Z6peZxagcXFRfbt28fRo0cBZrFhOr0NT58+DbDWhrnYsB2cT/OzYX42zM+G+bmuyc+G7eB8mp8N1TXMT9A+BMyVUt4upVwGngd2re6wVKcTJ04wNTXF5s2bAQo2TKe34eTkJMAlbJiKDdvB+TQ/G+Znw/xsmJ/rmvxs2A7Op/nZUF3DbNCuB8713D9fPdbvixHxRkS8EBEbahmdajE/P8+GDVclsWEyAxpexoap2LAdnE/zs2F+NszPhvm5rsnPhu3gfJqfDdVV1y8J+z6wsZTyAPBD4LlBT4qIvRExExEzFy5cqOmlVRMb5mfD/IZqCHYcc16L+dkwPxvmZ8P8bJifa9N28FrMz4a3gGE2aOeB3t35u6vHriilvFdKWajuPgNsG3SiUsrTpZTpUsr0unXrljNeLcP69es5d673h6BtmM2AhpPYMJU6G1bPtWMDnE/zs2F+NszPhvm5Ns3PtWk7OJ/mZ0N1DbNB+xpwT0RsiohJ4FHgSO8TIuKunrs7gTfrG6JW6sEHH+Stt97inXfeAQhsmE5vw8uXLwOsxYap2LAdnE/zs2F+NszPhvm5rsnPhu3gfJqfDdU1caMnlFI+jIj9wEvAGuDZUspsRDwJzJRSjgBfjoidwId0Plx8zyqOWTdpYmKCAwcOsH37doD7gK/bMJfehouLiwCXbJiLDdvB+TQ/G+Znw/xsmJ/rmvxs2A7Op/nZUF1RSmnkhaenp8vMzEwjr30ri4iTpZTpOs5lw+bU1dGGzfFazM+G+dkwPxvmZ8N2cG2an9difjbMz4b5raRhXb8kTJIkSZIkSZJ0k9yglSRJkiRJkqSGuEErSZIkSZIkSQ1xg1aSJEmSJEmSGuIGrSRJkiRJkiQ1xA1aSZIkSZIkSWqIG7SSJEmSJEmS1BA3aCVJkiRJkiSpIW7QSpIkSZIkSVJD3KCVJEmSJEmSpIa4QStJkiRJkiRJDXGDVpIkSZIkSZIa4gatJEmSJEmSJDXEDVpJkiRJkiRJaogbtJIkSZIkSZLUEDdoJUmSJEmSJKkhQ23QRsSOiDgTEXMR8cSA47dFxOHq+KsRsbH2kWpFjh07xpYtWwDut2FO3YZTU1MAd/Yft+H4s2E7OJ/mZ8P8bJifDfNzXZOfDdvB+TQ/GwqG2KCNiDXAU8DDwFZgd0Rs7XvaY8DPSilTwDeBb9Q9UC3f4uIi+/bt4+jRowCz2DCd3oanT58GWGvDXGzYDs6n+dkwPxvmZ8P8XNfkZ8N2cD7Nz4bqGuYnaB8C5kopb5dSLgPPA7v6nrMLeK76+gXg8xER9Q1TK3HixAmmpqbYvHkzQMGG6fQ2nJycBLiEDVOxYTs4n+Znw/xsmJ8N83Ndk58N28H5ND8bqmuYDdr1wLme++erxwY+p5TyIfA+8Ik6BqiVm5+fZ8OGDb0P2TCZAQ0vY8NUbNgOzqf52TA/G+Znw/xc1+Rnw3ZwPs3PhuqaGOWLRcReYG91dyEiTo3y9Wt0B3Cx6UHchF8FPnbw4MF3gS0rOZENG9PbEOC+5Z6oRQ0hV8faGkKrOmZqCM6ng9iww4ajY8OlbNhhw9FxbTpYpo6uTQfL1BCcTwexYYcNm7HshsNs0M4Dvdv5d1ePDXrO+YiYAD4OvNd/olLK08DTABExU0qZXs6gm5Zt7BHxWeBrpZTtETGDDdONvbdhdf88t3hDyDX+OhtCezpmG7vz6VLZxm7DpbKN3YZLZRu7DZfKNnbXpoNlGr9r08Gyjd35dKlsY7fhUtnHvtzvHeYjDl4D7omITRExCTwKHOl7zhHgD6uvvwT8fSmlLHdQqt2VhkBgw4z6r8O12DAbG7aD82l+NszPhvnZMD/XNfnZsB2cT/OzoYAhNmirz7fYD7wEvAl8t5QyGxFPRsTO6mkHgU9ExBzwZ8ATqzVg3by+hvdhw3QGXIeXbJiLDdvB+TQ/G+Znw/xsmJ/rmvxs2A7Op/nZUF3R1KZ7ROytfvw6Hcde/7lGLfPYob7x++fQHK/FDsde/7lGzbHXf65Rc+z1n2vUHHv95xq1zGMH16Zdmcfvtdjh2Os/16g59vrPNWq36thvuEEbEc8CjwA/LaXcP+B4AP8T+ALwc2BPKeX/LGcwWh02bAc75mfD/GyYnw3zs2E72DE/G+Znw/xsmJ8N1TXMZ9AeAnZc5/jDwD3VbS/w1ysflmp2CBu2wSHsmN0hbJjdIWyY3SFsmN0hbNgGh7BjdoewYXaHsGF2h7BhdoewoRjuM2h/BFy6zlN2AX9TOv4B+JWIuKt7MCJ2RMSZiJiLiCWfkxERt0XE4er4qxGxcRn/HqtiiLHviYgLEfF6dXu8iXEOEhHPRsRPI+LUoIbR8VfVZ5j8b+BHNhzfhrD0WuxtGBFvAI9xjWvRhs2wYYcNnU+bZMOP2PDK823YAN8TO2xowybZsMOGvic2yYYfaUvDAcevahgRnxrqxKWUG96AjcCpaxx7EfgfPfdfBqarr9cA/wJsBiaBfwS29n3/nwLfqr5+FDg8zJhW+zbk2PcAB5oe6zXG/xvAp7rd+hvS+fH4o0AArwCzNhyvW3/D/o59DT8D/GzQtWhDG9qw+YY38WdhRxva0Iata1hnRxva0IY2tKHviZk72rCdDfuO9zd8dZjzDvVLwqod9hfL4M/DeBH4y1LKK9X9l4GvlFJmIuKzwNdKKdurY38LPAT82+23377t3nvvveFrqx4LCwvMzc3xwQcfXCylrIuIbwPHSynfqRo+AHy6lPITG46v3o7A31I1BIiI/wB+r5Tyd9X9l4GvAL+EDcdGTQ3/HHgQ+HUAO47Wcho6n44XG+Znw3ZYWFjg1KlTi6WUid61KbiuycK1aX6uTfPzPTE/G7bHyZMnBzU8A3yulPKT633vRA2vPw9s6Ll/d/UYwHrgXM+x7wE/LqXsn56eLjMzMzW8vIZx9uxZHnnkEWZnZ9+tHuptM0/nf7FZD/wEG46tvo79bd4H7gP+rrrf7fjfseHYqKnheWB9KeV3AOw4WstsyIDnei02xIb52bAdzp49y6ZNm35R3XVdk5Br0/xcm+bne2J+NmyPiBjU8Dwf7bdd0zC/JOxGjgB/UH3GwmeA92+0K6yxcwS4E8CGqV0AfstrMTUb5mfD/GyYnw3bwY752TA/G+Znw/xseIu44U/QRsR3gM8Bd0TEeeAv6PxfGiilfAv4AZ3PV5gDfg78Uc+3X++nazUiu3fv5vjx41y8eBHggYh4DPgYnc/zeIVOwzXAC8D/w4Zjqb8j8B/Anoi4v7oW/xvwf1l6LdpwTNgwvxU0BDuOBRvmZ8N26HYEbqv+jjGH74mpuK7Jz4b5+Z6Ynw1baXldVvmDcyeAt4FNfPShv/eVUti2bVvR6AEznX/wW1z9ocUnig3TAGZsmFtdDYsdG3MzDYvX4liyYX42zM+1aTu4Ns3PtWl+vifmZ8P8brZh762Ojzi4plLKh8B+4CXgTeC7pZTZiHhyNV9X17Wu+ucP6FzIc8D/ovOb/Zaw4VizYX4rbhgRO0c1WA00VEPwWhxjNszPhrm5Nm0PG+bn2jQ/3xPzs2F+QzfsFZ0N3tHzA4ubEREnSynTdZzLhs2pq6MNm+O1mJ8N87NhfjbMz4bt4No0P6/F/GyYnw3zW0nDVf0JWkmSJEmSJEnStblBK0mSJEmSJEkNcYNWkiRJkiRJkhriBq0kSZIkSZIkNcQNWkmSJEmSJElqiBu0kiRJkiRJktQQN2glSZIkSZIkqSFu0EqSJEmSJElSQ9yglSRJkiRJkqSGuEErSZIkSZIkSQ1xg1aSJEmSJEmSGuIGrSRJkiRJkiQ1xA1aSZIkSZIkSWqIG7SSJEmSJEmS1JChNmgjYkdEnImIuYh4YsDxPRFxISJer26P1z9UrcSxY8fYsmULwP02zKnbcGpqCuDO/uM2HH82bAfn0/xsmJ8N87Nhfq5r8rNhOzif5mdDwRAbtBGxBngKeBjYCuyOiK0Dnnq4lPLJ6vZMzePUCiwuLrJv3z6OHj0KMIsN0+ltePr0aYC1NszFhu3gfJqfDfOzYX42zM91TX42bAfn0/xsqK5hfoL2IWCulPJ2KeUy8Dywa3WHpTqdOHGCqakpNm/eDFCwYTq9DScnJwEuYcNUbNgOzqf52TA/G+Znw/xc1+Rnw3ZwPs3PhuoaZoN2PXCu5/756rF+X4yINyLihYjYUMvoVIv5+Xk2bLgqiQ2TGdDwMjZMxYbt4Hyanw3zs2F+NszPdU1+NmwH59P8bKiuun5J2PeBjaWUB4AfAs8NelJE7I2ImYiYuXDhQk0vrZrYMD8b5jdUQ7DjmPNazM+G+dkwPxvmZ8P8XJu2g9difja8BQyzQTsP9O7O3109dkUp5b1SykJ19xlg26ATlVKeLqVMl1Km161bt5zxahnWr1/PuXO9PwRtw2wGNJzEhqnU2bB6rh0b4Hyanw3zs2F+NszPtWl+rk3bwfk0Pxuqa5gN2teAeyJiU0RMAo8CR3qfEBF39dzdCbxZ3xC1Ug8++CBvvfUW77zzDkBgw3R6G16+fBlgLTZMxYbt4Hyanw3zs2F+NszPdU1+NmwH59P8bKiuiRs9oZTyYUTsB14C1gDPllJmI+JJYKaUcgT4ckTsBD6k8+Hie1ZxzLpJExMTHDhwgO3btwPcB3zdhrn0NlxcXAS4ZMNcbNgOzqf52TA/G+Znw/xc1+Rnw3ZwPs3PhuqKUkojLzw9PV1mZmYaee1bWUScLKVM13EuGzanro42bI7XYn42zM+G+dkwPxu2g2vT/LwW87NhfjbMbyUN6/olYZIkSZIkSZKkm+QGrSRJkiRJkiQ1xA1aSZIkSZIkSWqIG7SSJEmSJEmS1BA3aCVJkiRJkiSpIW7QSpIkSZIkSVJD3KCVJEmSJEmSpIa4QStJkiRJkiRJDXGDVpIkSZIkSZIa4gatJEmSJEmSJDXEDVpJkiRJkiRJaogbtJIkSZIkSZLUEDdoJUmSJEmSJKkhbtBKkiRJkiRJUkPcoJUkSZIkSZKkhgy1QRsROyLiTETMRcQTA47fFhGHq+OvRsTG2keqFTl27BhbtmwBuN+GOXUbTk1NAdzZf9yG48+G7eB8mp8N87NhfjbMz3VNfjZsB+fT/GwoGGKDNiLWAE8BDwNbgd0RsbXvaY8BPyulTAHfBL5R90C1fIuLi+zbt4+jR48CzGLDdHobnj59GmCtDXOxYTs4n+Znw/xsmJ8N83Ndk58N28H5ND8bqmuYn6B9CJgrpbxdSrkMPA/s6nvOLuC56usXgM9HRNQ3TK3EiRMnmJqaYvPmzQAFG6bT23BychLgEjZMxYbt4Hyanw3zs2F+NszPdU1+NmwH59P8bKiuYTZo1wPneu6frx4b+JxSyofA+8An6higVm5+fp4NGzb0PmTDZAY0vIwNU7FhOzif5mfD/GyYnw3zc12Tnw3bwfk0Pxuqa2KULxYRe4G91d2FiDg1ytev0R3AxaYHcRN+FfjYwYMH3wW2rORENmxMb0OA+5Z7ohY1hFwda2sIreqYqSE4nw5iww4bjo4Nl7Jhhw1Hx7XpYJk6ujYdLFNDcD4dxIYdNmzGshsOs0E7D/Ru599dPTboOecjYgL4OPBe/4lKKU8DTwNExEwpZXo5g25atrFHxGeBr5VStkfEDDZMN/behtX989ziDSHX+OtsCO3pmG3szqdLZRu7DZfKNnYbLpVt7DZcKtvYXZsOlmn8rk0HyzZ259Olso3dhktlH/tyv3eYjzh4DbgnIjZFxCTwKHCk7zlHgD+svv4S8PellLLcQal2VxoCgQ0z6r8O12LDbGzYDs6n+dkwPxvmZ8P8XNfkZ8N2cD7Nz4YChtigrT7fYj/wEvAm8N1SymxEPBkRO6unHQQ+ERFzwJ8BT6zWgHXz+hrehw3TGXAdXrJhLjZsB+fT/GyYnw3zs2F+rmvys2E7OJ/mZ0N1RVOb7hGxt/rx63Qce/3nGrXMY4f6xu+fQ3O8Fjsce/3nGjXHXv+5Rs2x13+uUXPs9Z9r1DKPHVybdmUev9dih2Ov/1yj5tjrP9eo3apjb2yDVpIkSZIkSZJudTf8iIOIeDYifhrX+O1v0fFXETEXEW9ExKfqH6ZWwobtYMf8bJifDfOzYX42bAc75mfD/GyYnw3zs6G6hvklYYeAHdc5/jBwT3XbC/x178GI2BERZ6r/mJZ8TkZE3BYRh6vjr0bExqFHv8qGGPueiLgQEa9Xt8ebGOcgfRf5Ifoa9l7kwNvANmw4zg2hr2P/RE3nc2sGXos2bIYNO2x41fnsOGI2/IgNrzzfhg3wPbHDhleea8MG2LDDhledz44jZsOPtKhh//GrGsawm+qllBvegI3AqWsc+zawu+f+GeCu6us1wL8Am4FJ4B+BrX3f/6fAt6qvHwUODzOm1b4NOfY9wIGmx3qN8f8G8Klut/6GwBeAo0AA3wPmbDhet/6G/R37Gn4G+PdB16INbWjD5hvexJ+FHW1oQxu2rmGdHW1oQxva0Ia+J2buaMN2Nuw73t/w1WHOO8xP0N7IeuBcz/3z1WMAD9HZ+Hu7lHIZeB7Y1ff9u4Dnqq9fAD4fEVHDuFZqmLGPrVLKj4BL13nKLuBvSue/nl8CJiPiruqYDcfAzTQspfwDcDvw857j3Y42bIgNARs6nzbMhlfYsMOGDfE9EbChDRtmQ8CGvic2zIZX3EoNf6Vnv+2ahvolYdWPQL9YSrl/wLEXgb8spbxS3X8Z+EopZSYivgTsKKU8Xh07CPw28K+33377tnvvvfeGr616LCwsMDc3xwcffHCxlLKut1v19Z3An1TdbDimejsCr3L1tXeRTreD1f2Xga/Q+V/jbDgmamr4+8AfA78MYMfRWk5D59PxYsP8bNgOCwsLnDp1arGUMjHg7xSuaxJwbZqfa9P8fE/Mz4btcfLkyUENrzS73vdO1PD688CGnvt3V48Nchz4r1LK/unp6TIzc92xqUZnz57lkUceYXZ29t0Bh+fpvMl22XBM3aDjAvBrPfe7HTf2Pe84NmxMTQ0BZksp+wHsOFrLbDjIcbwWG2HD/GzYDmfPnmXTpk2/uMZh1zUJuDbNz7Vpfr4n5mfD9oiIQQ2HUsdHHBwB/qD6ENzPAO+XUn5SHbuZzVuNVm+bI3TeZOdtmEp/mwL85oBr0Ybjy4b5Ddtw0HPtOB5smJ8N28H3xPxsmJ8N8/M9MT8b5resLjf8CdqI+A7wOeCOiDgP/AWdzyyllPIt4Ad0PgB3js7nYvxRz7e/BtwTEZuqwTwK/O6N/11Up927d3P8+HEuXrwI8EBEPAb8AvhqRDwPvAf8J/AKNhxb/R3p9PpqRHwceB34MfAmS69FG44JG+a3goZgx7Fgw/xs2A7djsBt1d8xXsD3xFRc1+Rnw/x8T8zPhq10BNhf7bd9mqs31a+trP5vN/sC8M90fjvbV6vHnty2bVvR6AHvdv5BAE9VXf4JmC42TAOYsWFudTUEdtqxGTfbsHgtjh0b5mfD/FybtoNr0/xcm+bne2J+NsxvOQ27t6F+Sdhq8PMwmhERJ0sp03Wcy4bNqaujDZvjtZifDfOzYX42zM+G7eDaND+vxfxsmJ8N81tJwzo+g1aSJEmSJEmStAxu0EqSJEmSJElSQ9yglSRJkiRJkqSGuEErSZIkSZIkSQ1xg1aSJEmSJEmSGuIGrSRJkiRJkiQ1xA1aSZIkSZIkSWqIG7SSJEmSJEmS1BA3aCVJkiRJkiSpIW7QSpIkSZIkSVJD3KCVJEmSJEmSpIa4QStJkiRJkiRJDXGDVpIkSZIkSZIa4gatJEmSJEmSJDVkqA3aiNgREWciYi4inhhwfE9EXIiI16vb4/UPVStx7NgxtmzZAnC/DXPqNpyamgK4s/+4DcefDdvB+TQ/G+Znw/xsmJ/rmvxs2A7Op/nZUDDEBm1ErAGeAh4GtgK7I2LrgKceLqV8sro9U/M4tQKLi4vs27ePo0ePAsxiw3R6G54+fRpgrQ1zsWE7OJ/mZ8P8bJifDfNzXZOfDdvB+TQ/G6prmJ+gfQiYK6W8XUq5DDwP7FrdYalOJ06cYGpqis2bNwMUbJhOb8PJyUmAS9gwFRu2g/NpfjbMz4b52TA/1zX52bAdnE/zs6G6htmgXQ+c67l/vnqs3xcj4o2IeCEiNtQyOtVifn6eDRuuSmLDZAY0vIwNU7FhOzif5mfD/GyYnw3zc12Tnw3bwfk0Pxuqq65fEvZ9YGMp5QHgh8Bzg54UEXsjYiYiZi5cuFDTS6smNszPhvkN1RDsOOa8FvOzYX42zM+G+dkwP9em7eC1mJ8NbwHDbNDOA72783dXj11RSnmvlLJQ3X0G2DboRKWUp0sp06WU6XXr1i1nvFqG9evXc+5c7w9B2zCbAQ0nsWEqdTasnmvHBjif5mfD/GyYnw3zc22an2vTdnA+zc+G6hpmg/Y14J6I2BQRk8CjwJHeJ0TEXT13dwJv1jdErdSDDz7IW2+9xTvvvAMQ2DCd3oaXL18GWIsNU7FhOzif5mfD/GyYnw3zc12Tnw3bwfk0Pxuqa+JGTyilfBgR+4GXgDXAs6WU2Yh4EpgppRwBvhwRO4EP6Xy4+J5VHLNu0sTEBAcOHGD79u0A9wFft2EuvQ0XFxcBLtkwFxu2g/NpfjbMz4b52TA/1zX52bAdnE/zs6G6opTSyAtPT0+XmZmZRl77VhYRJ0sp03Wcy4bNqaujDZvjtZifDfOzYX42zM+G7eDaND+vxfxsmJ8N81tJw7p+SZgkSZIkSZIk6Sa5QStJkiRJkiRJDXGDVpIkSZIkSZIa4gatJEmSJEmSJDXEDVpJkiRJkiRJaogbtJIkSZIkSZLUEDdoJUmSJEmSJKkhbtBKkiRJkiRJUkPcoJUkSZIkSZKkhrhBK0mSJEmSJEkNcYNWkiRJkiRJkhriBq0kSZIkSZIkNcQNWkmSJEmSJElqiBu0kiRJkiRJktQQN2glSZIkSZIkqSFu0EqSJEmSJElSQ4baoI2IHRFxJiLmIuKJAcdvi4jD1fFXI2Jj7SPVihw7dowtW7YA3G/DnLoNp6amAO7sP27D8WfDdnA+zc+G+dkwPxvm57omPxu2g/NpfjYUDLFBGxFrgKeAh4GtwO6I2Nr3tMeAn5VSpoBvAt+oe6BavsXFRfbt28fRo0cBZrFhOr0NT58+DbDWhrnYsB2cT/OzYX42zM+G+bmuyc+G7eB8mp8N1TXMT9A+BMyVUt4upVwGngd29T1nF/Bc9fULwOcjIuobplbixIkTTE1NsXnzZoCCDdPpbTg5OQlwCRumYsN2cD7Nz4b52TA/G+bnuiY/G7aD82l+NlTXMBu064FzPffPV48NfE4p5UPgfeATdQxQKzc/P8+GDRt6H7JhMgMaXsaGqdiwHZxP87NhfjbMz4b5ua7Jz4bt4Hyanw3VNTHKF4uIvcDe6u5CRJwa5evX6A7gYtODuAm/Cnzs4MGD7wJbVnIiGzamtyHAfcs9UYsaQq6OtTWEVnXM1BCcTwexYYcNR8eGS9mww4aj49p0sEwdXZsOlqkhOJ8OYsMOGzZj2Q2H2aCdB3q38++uHhv0nPMRMQF8HHiv/0SllKeBpwEiYqaUMr2cQTct29gj4rPA10op2yNiBhumG3tvw+r+eW7xhpBr/HU2hPZ0zDZ259Olso3dhktlG7sNl8o2dhsulW3srk0HyzR+16aDZRu78+lS2cZuw6Wyj3253zvMRxy8BtwTEZsiYhJ4FDjS95wjwB9WX38J+PtSSlnuoFS7Kw2BwIYZ9V+Ha7FhNjZsB+fT/GyYnw3zs2F+rmvys2E7OJ/mZ0MBQ2zQVp9vsR94CXgT+G4pZTYinoyIndXTDgKfiIg54M+AJ1ZrwLp5fQ3vw4bpDLgOL9kwFxu2g/NpfjbMz4b52TA/1zX52bAdnE/zs6G6oqlN94jYW/34dTqOvf5zjVrmsUN94/fPoTleix2Ovf5zjZpjr/9co+bY6z/XqDn2+s81apnHDq5NuzKP32uxw7HXf65Rc+z1n2vUbtWx33CDNiKeBR4BflpKuX/A8QD+J/AF4OfAnlLK/1nOYLQ6bNgOdszPhvnZMD8b5mfDdrBjfjbMz4b52TA/G6prmM+gPQTsuM7xh4F7qtte4K9XPizV7BA2bIND2DG7Q9gwu0PYMLtD2DC7Q9iwDQ5hx+wOYcPsDmHD7A5hw+wOYUMx3GfQ/gi4dJ2n7AL+pnT8A/ArEXFX92BE7IiIMxExFxFLPicjIm6LiMPV8VcjYuMy/j1WxRBj3xMRFyLi9er2eBPjHCQino2In0bEqUENo+Ovqs8w+d/Aj2w4vg1h6bXY2zAi3gAe4xrXog2bYcMOGzqfNsmGH7HhlefbsAG+J3bY0IZNsmGHDX1PbJINP9KWhgOOX9UwIj411IlLKTe8ARuBU9c49iLwP3ruvwxMV1+vAf4F2AxMAv8IbO37/j8FvlV9/ShweJgxrfZtyLHvAQ40PdZrjP83gE91u/U3pPPj8UeBAF4BZm04Xrf+hv0d+xp+BvjZoGvRhja0YfMNb+LPwo42tKENW9ewzo42tKENbWhD3xMzd7RhOxv2He9v+Oow5x3ql4RVO+wvlsGfh/Ei8JellFeq+y8DXymlzETEZ4GvlVK2V8f+FngI+Lfbb79927333nvD11Y9FhYWmJub44MPPrhYSlkXEd8GjpdSvlM1fAD4dCnlJzYcX70dgb+laggQEf8B/F4p5e+q+y8DXwF+CRuOjZoa/jnwIPDrAHYcreU0dD4dLzbMz4btsLCwwKlTpxZLKRO9a1NwXZOFa9P8XJvm53tifjZsj5MnTw5qeAb4XCnlJ9f73okaXn8e2NBz/+7qMYD1wLmeY98DflxK2T89PV1mZmZqeHkN4+zZszzyyCPMzs6+Wz3U22aezv9isx74CTYcW30d+9u8D9wH/F11v9vxv2PDsVFTw/PA+lLK7wDYcbSW2ZABz/VabIgN87NhO5w9e5ZNmzb9orrruiYh16b5uTbNz/fE/GzYHhExqOF5Ptpvu6ZhfknYjRwB/qD6jIXPAO/faFdYY+cIcCeADVO7APyW12JqNszPhvnZMD8btoMd87NhfjbMz4b52fAWccOfoI2I7wCfA+6IiPPAX9D5vzRQSvkW8AM6n68wB/wc+KOeb7/eT9dqRHbv3s3x48e5ePEiwAMR8RjwMTqf5/EKnYZrgBeA/4cNx1J/R+A/gD0RcX91Lf434P+y9Fq04ZiwYX4raAh2HAs2zM+G7dDtCNxW/R1jDt8TU3Fdk58N8/M9MT8bttLyuqzyB+dOAG8Dm/joQ3/vK6Wwbdu2otEDZjr/4Le4+kOLTxQbpgHM2DC3uhoWOzbmZhoWr8WxZMP8bJifa9N2cG2an2vT/HxPzM+G+d1sw95bHR9xcE2llA+B/cBLwJvAd0spsxHx5Gq+rq5rXfXPH9C5kOeA/0XnN/stYcOxZsP8VtwwInaOarAaaKiG4LU4xmyYnw1zc23aHjbMz7Vpfr4n5mfD/IZu2Cs6G7yj5wcWNyMiTpZSpus4lw2bU1dHGzbHazE/G+Znw/xsmJ8N28G1aX5ei/nZMD8b5reShqv6E7SSJEmSJEmSpGtzg1aSJEmSJEmSGuIGrSRJkiRJkiQ1xA1aSZIkSZIkSWqIG7SSJEmSJEmS1BA3aCVJkiRJkiSpIW7QSpIkSZIkSVJD3KCVJEmSJEmSpIa4QStJkiRJkiRJDXGDVpIkSZIkSZIa4gatJEmSJEmSJDXEDVpJkiRJkiRJaogbtJIkSZIkSZLUEDdoJUmSJEmSJKkhQ23QRsSOiDgTEXMR8cSA43si4kJEvF7dHq9/qFqJY8eOsWXLFoD7bZhTt+HU1BTAnf3HbTj+bNgOzqf52TA/G+Znw/xc1+Rnw3ZwPs3PhoIhNmgjYg3wFPAwsBXYHRFbBzz1cCnlk9XtmZrHqRVYXFxk3759HD16FGAWG6bT2/D06dMAa22Yiw3bwfk0PxvmZ8P8bJif65r8bNgOzqf52VBdw/wE7UPAXCnl7VLKZeB5YNfqDkt1OnHiBFNTU2zevBmgYMN0ehtOTk4CXMKGqdiwHZxP87NhfjbMz4b5ua7Jz4bt4Hyanw3VNcwG7XrgXM/989Vj/b4YEW9ExAsRsaGW0akW8/PzbNhwVRIbJjOg4WVsmIoN28H5ND8b5mfD/GyYn+ua/GzYDs6n+dlQXXX9krDvAxtLKQ8APwSeG/SkiNgbETMRMXPhwoWaXlo1sWF+NsxvqIZgxzHntZifDfOzYX42zM+G+bk2bQevxfxseAsYZoN2Hujdnb+7euyKUsp7pZSF6u4zwLZBJyqlPF1KmS6lTK9bt24549UyrF+/nnPnen8I2obZDGg4iQ1TqbNh9Vw7NsD5ND8b5mfD/GyYn2vT/FybtoPzaX42VNcwG7SvAfdExKaImAQeBY70PiEi7uq5uxN4s74haqUefPBB3nrrLd555x2AwIbp9Da8fPkywFpsmIoN28H5ND8b5mfD/GyYn+ua/GzYDs6n+dlQXRM3ekIp5cOI2A+8BKwBni2lzEbEk8BMKeUI8OWI2Al8SOfDxfes4ph1kyYmJjhw4ADbt28HuA/4ug1z6W24uLgIcMmGudiwHZxP87NhfjbMz4b5ua7Jz4bt4Hyanw3VFaWURl54enq6zMzMNPLat7KIOFlKma7jXDZsTl0dbdgcr8X8bJifDfOzYX42bAfXpvl5LeZnw/xsmN9KGtb1S8IkSZIkSZIkSTfJDVpJkiRJkiRJaogbtJIkSZIkSZLUEDdoJUmSJEmSJKkhbtBKkiRJkiRJUkPcoJUkSZIkSZKkhrhBK0mSJEmSJEkNcYNWkiRJkiRJkhriBq0kSZIkSZIkNcQNWkmSJEmSJElqiBu0kiRJkiRJktQQN2glSZIkSZIkqSFu0EqSJEmSJElSQ9yglSRJkiRJkqSGuEErSZIkSZIkSQ0ZaoM2InZExJmImIuIJwYcvy0iDlfHX42IjbWPVCty7NgxtmzZAnC/DXPqNpyamgK4s/+4DcefDdvB+TQ/G+Znw/xsmJ/rmvxs2A7Op/nZUDDEBm1ErAGeAh4GtgK7I2Jr39MeA35WSpkCvgl8o+6BavkWFxfZt28fR48eBZjFhun0Njx9+jTAWhvmYsN2cD7Nz4b52TA/G+bnuiY/G7aD82l+NlTXMD9B+xAwV0p5u5RyGXge2NX3nF3Ac9XXLwCfj4iob5haiRMnTjA1NcXmzZsBCjZMp7fh5OQkwCVsmIoN28H5ND8b5mfD/GyYn+ua/GzYDs6n+dlQXcNs0K4HzvXcP189NvA5pZQPgfeBT9QxQK3c/Pw8GzZs6H3IhskMaHgZG6Ziw3ZwPs3PhvnZMD8b5ue6Jj8btoPzaX42VNfEKF8sIvYCe6u7CxFxapSvX6M7gItND+Im/CrwsYMHD74LbFnJiWzYmN6GAPct90Qtagi5OtbWEFrVMVNDcD4dxIYdNhwdGy5lww4bjo5r08EydXRtOlimhuB8OogNO2zYjGU3HGaDdh7o3c6/u3ps0HPOR8QE8HHgvf4TlVKeBp4GiIiZUsr0cgbdtGxjj4jPAl8rpWyPiBlsmG7svQ2r++e5xRtCrvHX2RDa0zHb2J1Pl8o2dhsulW3sNlwq29htuFS2sbs2HSzT+F2bDpZt7M6nS2Ubuw2Xyj725X7vMB9x8BpwT0RsiohJ4FHgSN9zjgB/WH39JeDvSylluYNS7a40BAIbZtR/Ha7FhtnYsB2cT/OzYX42zM+G+bmuyc+G7eB8mp8NBQyxQVt9vsV+4CXgTeC7pZTZiHgyInZWTzsIfCIi5oA/A55YrQHr5vU1vA8bpjPgOrxkw1xs2A7Op/nZMD8b5mfD/FzX5GfDdnA+zc+G6oqmNt0jYm/149fpOPb6zzVqmccO9Y3fP4fmeC12OPb6zzVqjr3+c42aY6//XKPm2Os/16hlHju4Nu3KPH6vxQ7HXv+5Rs2x13+uUbtVx97YBq0kSZIkSZIk3epu+BEHEfFsRPw0rvHb36LjryJiLiLeiIhP1T9MrYQN28GO+dkwPxvmZ8P8bNgOdszPhvnZMD8b5mdDdQ3zS8IOATuuc/xh4J7qthf4696DEbEjIs5U/zEt+ZyMiLgtIg5Xx1+NiI1Dj36VDTH2PRFxISJer26PNzHOQfou8kP0Ney9yIG3gW3YcJwbQl/H/omazufWDLwWbdgMG3bY8Krz2XHEbPgRG155vg0b4Htihw2vPNeGDbBhhw2vOp8dR8yGH2lRw/7jVzWMYTfVSyk3vAEbgVPXOPZtYHfP/TPAXdXXa4B/ATYDk8A/Alv7vv9PgW9VXz8KHB5mTKt9G3Lse4ADTY/1GuP/DeBT3W79DYEvAEeBAL4HzNlwvG79Dfs79jX8DPDvg65FG9rQhs03vIk/Czva0IY2bF3DOjva0IY2tKENfU/M3NGG7WzYd7y/4avDnHeYn6C9kfXAuZ7756vHAB6is/H3dinlMvA8sKvv+3cBz1VfvwB8PiKihnGt1DBjH1ullB8Bl67zlF3A35TOfz2/BExGxF3VMRuOgZtpWEr5B+B24Oc9x7sdbdgQGwI2dD5tmA2vsGGHDRvieyJgQxs2zIaADX1PbJgNr7iVGv5Kz37bNQ31S8KqH4F+sZRy/4BjLwJ/WUp5pbr/MvCVUspMRHwJ2FFKebw6dhD4beBfb7/99m333nvvDV9b9VhYWGBubo4PPvjgYillXW+36us7gT+putlwTPV2BF7l6mvvIp1uB6v7LwNfofO/xtlwTNTU8PeBPwZ+GcCOo7Wchs6n48WG+dmwHRYWFjh16tRiKWViwN8pXNck4No0P9em+fmemJ8N2+PkyZODGl5pdr3vnajh9eeBDT33764eG+Q48F+llP3T09NlZua6Y1ONzp49yyOPPMLs7Oy7Aw7P03mT7bLhmLpBxwXg13rudztu7HvecWzYmJoaAsyWUvYD2HG0ltlwkON4LTbChvnZsB3Onj3Lpk2bfnGNw65rEnBtmp9r0/x8T8zPhu0REYMaDqWOjzg4AvxB9SG4nwHeL6X8pDp2M5u3Gq3eNkfovMnO2zCV/jYF+M0B16INx5cN8xu24aDn2nE82DA/G7aD74n52TA/G+bne2J+NsxvWV1u+BO0EfEd4HPAHRFxHvgLOp9ZSinlW8AP6HwA7hydz8X4o55vfw24JyI2VYN5FPjdG/+7qE67d+/m+PHjXLx4EeCBiHgM+AXw1Yh4HngP+E/gFWw4tvo70un11Yj4OPA68GPgTZZeizYcEzbMbwUNwY5jwYb52bAduh2B26q/Y7yA74mpuK7Jz4b5+Z6Ynw1b6Qiwv9pv+zRXb6pfW1n93272BeCf6fx2tq9Wjz25bdu2otED3u38gwCeqrr8EzBdbJgGMGPD3OpqCOy0YzNutmHxWhw7NszPhvm5Nm0H16b5uTbNz/fE/GyY33Iadm9D/ZKw1eDnYTQjIk6WUqbrOJcNm1NXRxs2x2sxPxvmZ8P8bJifDdvBtWl+Xov52TA/G+a3koZ1fAatJEmSJEmSJGkZ3KCVJEmSJEmSpIa4QStJkiRJkiRJDXGDVpIkSZIkSZIa4gatJEmSJEmSJDXEDVpJkiRJkiRJaogbtJIkSZIkSZLUEDdoJUmSJEmSJKkhbtBKkiRJkiRJUkPcoJUkSZIkSZKkhrhBK0mSJEmSJEkNcYNWkiRJkiRJkhriBq0kSZIkSZIkNcQNWkmSJEmSJElqyFAbtBGxIyLORMRcRDwx4PieiLgQEa9Xt8frH6pW4tixY2zZsgXgfhvm1G04NTUFcGf/cRuOPxu2g/NpfjbMz4b52TA/1zX52bAdnE/zs6FgiA3aiFgDPAU8DGwFdkfE1gFPPVxK+WR1e6bmcWoFFhcX2bdvH0ePHgWYxYbp9DY8ffo0wFob5mLDdnA+zc+G+dkwPxvm57omPxu2g/NpfjZU1zA/QfsQMFdKebuUchl4Hti1usNSnU6cOMHU1BSbN28GKNgwnd6Gk5OTAJewYSo2bAfn0/xsmJ8N87Nhfq5r8rNhOzif5mdDdQ2zQbseONdz/3z1WL8vRsQbEfFCRGyoZXSqxfz8PBs2XJXEhskMaHgZG6Ziw3ZwPs3PhvnZMD8b5ue6Jj8btoPzaX42VFddvyTs+8DGUsoDwA+B5wY9KSL2RsRMRMxcuHChppdWTWyYnw3zG6oh2HHMeS3mZ8P8bJifDfOzYX6uTdvBazE/G94ChtmgnQd6d+fvrh67opTyXillobr7DLBt0IlKKU+XUqZLKdPr1q1bzni1DOvXr+fcud4fgrZhNgMaTmLDVOpsWD3Xjg1wPs3PhvnZMD8b5ufaND/Xpu3gfJqfDdU1zAbta8A9EbEpIiaBR4EjvU+IiLt67u4E3qxviFqpBx98kLfeeot33nkHILBhOr0NL1++DLAWG6Ziw3ZwPs3PhvnZMD8b5ue6Jj8btoPzaX42VNfEjZ5QSvkwIvYDLwFrgGdLKbMR8SQwU0o5Anw5InYCH9L5cPE9qzhm3aSJiQkOHDjA9u3bAe4Dvm7DXHobLi4uAlyyYS42bAfn0/xsmJ8N87Nhfq5r8rNhOzif5mdDdUUppZEXnp6eLjMzM4289q0sIk6WUqbrOJcNm1NXRxs2x2sxPxvmZ8P8bJifDdvBtWl+Xov52TA/G+a3koZ1/ZIwSZIkSZIkSdJNcoNWkiRJkiRJkhriBq0kSZIkSZIkNcQNWkmSJEmSJElqiBu0kiRJkiRJktQQN2glSZIkSZIkqSFu0EqSJEmSJElSQ9yglSRJkiRJkqSGuEErSZIkSZIkSQ1xg1aSJEmSJEmSGuIGrSRJkiRJkiQ1xA1aSZIkSZIkSWqIG7SSJEmSJEmS1BA3aCVJkiRJkiSpIW7QSpIkSZIkSVJD3KCVJEmSJEmSpIYMtUEbETsi4kxEzEXEEwOO3xYRh6vjr0bExtpHqhU5duwYW7ZsAbjfhjl1G05NTQHc2X/chuPPhu3gfJqfDfOzYX42zM91TX42bAfn0/xsKBhigzYi1gBPAQ8DW4HdEbG172mPAT8rpUwB3wS+UfdAtXyLi4vs27ePo0ePAsxiw3R6G54+fRpgrQ1zsWE7OJ/mZ8P8bJifDfNzXZOfDdvB+TQ/G6prmJ+gfQiYK6W8XUq5DDwP7Op7zi7guerrF4DPR0TUN0ytxIkTJ5iammLz5s0ABRum09twcnIS4BI2TMWG7eB8mp8N87NhfjbMz3VNfjZsB+fT/GyormE2aNcD53run68eG/icUsqHwPvAJ+oYoFZufn6eDRs29D5kw2QGNLyMDVOxYTs4n+Znw/xsmJ8N83Ndk58N28H5ND8bqmtilC8WEXuBvdXdhYg4NcrXr9EdwMWmB3ETfhX42MGDB98FtqzkRDZsTG9DgPuWe6IWNYRcHWtrCK3qmKkhOJ8OYsMOG46ODZeyYYcNR8e16WCZOro2HSxTQ3A+HcSGHTZsxrIbDrNBOw/0buffXT026DnnI2IC+DjwXv+JSilPA08DRMRMKWV6OYNuWraxR8Rnga+VUrZHxAw2TDf23obV/fPc4g0h1/jrbAjt6Zht7M6nS2Ubuw2XyjZ2Gy6Vbew2XCrb2F2bDpZp/K5NB8s2dufTpbKN3YZLZR/7cr93mI84eA24JyI2RcQk8ChwpO85R4A/rL7+EvD3pZSy3EGpdlcaAoENM+q/Dtdiw2xs2A7Op/nZMD8b5mfD/FzX5GfDdnA+zc+GAobYoK0+32I/8BLwJvDdUspsRDwZETurpx0EPhERc8CfAU+s1oB18/oa3ocN0xlwHV6yYS42bAfn0/xsmJ8N87Nhfq5r8rNhOzif5mdDdUVTm+4Rsbf68et0HHv95xq1zGOH+sbvn0NzvBY7HHv95xo1x17/uUbNsdd/rlFz7PWfa9Qyjx1cm3ZlHr/XYodjr/9co+bY6z/XqN2qY7/hBm1EPAs8Avy0lHL/gOMB/E/gC8DPgT2llP+znMFoddiwHeyYnw3zs2F+NszPhu1gx/xsmJ8N87NhfjZU1zCfQXsI2HGd4w8D91S3vcBfr3xYqtkhbNgGh7BjdoewYXaHsGF2h7BhdoewYRscwo7ZHcKG2R3ChtkdwobZHcKGYrjPoP0RcOk6T9kF/E3p+AfgVyLiru7BiNgREWciYi4ilnxORkTcFhGHq+OvRsTGZfx7rIohxr4nIi5ExOvV7fEmxjlIRDwbET+NiFODGkbHX1WfYfK/gR/ZcHwbwtJrsbdhRLwBPMY1rkUbNsOGHTZ0Pm2SDT9iwyvPt2EDfE/ssKENm2TDDhv6ntgkG36kLQ0HHL+qYUR8aqgTl1JueAM2AqeucexF4H/03H8ZmK6+XgP8C7AZmAT+Edja9/1/Cnyr+vpR4PAwY1rt25Bj3wMcaHqs1xj/bwCf6nbrb0jnx+OPAgG8AszacLxu/Q37O/Y1/Azws0HXog1taMPmG97En4UdbWhDG7auYZ0dbWhDG9rQhr4nZu5ow3Y27Dve3/DVYc471C8Jq3bYXyyDPw/jReAvSymvVPdfBr5SSpmJiM8CXyulbK+O/S3wEPBvt99++7Z77733hq+teiwsLDA3N8cHH3xwsZSyLiK+DRwvpXynavgA8OlSyk9sOL56OwJ/S9UQICL+A/i9UsrfVfdfBr4C/BI2HBs1Nfxz4EHg1wHsOFrLaeh8Ol5smJ8N22FhYYFTp04tllImetem4LomC9em+bk2zc/3xPxs2B4nT54c1PAM8LlSyk+u970TNbz+PLCh5/7d1WMA64FzPce+B/y4lLJ/enq6zMzM1PDyGsbZs2d55JFHmJ2dfbd6qLfNPJ3/xWY98BNsOLb6Ova3eR+4D/i76n6343/HhmOjpobngfWllN8BsONoLbMhA57rtdgQG+Znw3Y4e/YsmzZt+kV113VNQq5N83Ntmp/vifnZsD0iYlDD83y033ZNw/ySsBs5AvxB9RkLnwHev9GusMbOEeBOABumdgH4La/F1GyYnw3zs2F+NmwHO+Znw/xsmJ8N87PhLeKGP0EbEd8BPgfcERHngb+g839poJTyLeAHdD5fYQ74OfBHPd9+vZ+u1Yjs3r2b48ePc/HiRYAHIuIx4GN0Ps/jFToN1wAvAP8PG46l/o7AfwB7IuL+6lr8b8D/Zem1aMMxYcP8VtAQ7DgWbJifDduh2xG4rfo7xhy+J6biuiY/G+bne2J+Nmyl5XVZ5Q/OnQDeBjbx0Yf+3ldKYdu2bUWjB8x0/sFvcfWHFp8oNkwDmLFhbnU1LHZszM00LF6LY8mG+dkwP9em7eDaND/Xpvn5npifDfO72Ya9tzo+4uCaSikfAvuBl4A3ge+WUmYj4snVfF1d17rqnz+gcyHPAf+Lzm/2W8KGY82G+a24YUTsHNVgNdBQDcFrcYzZMD8b5ubatD1smJ9r0/x8T8zPhvkN3bBXdDZ4R88PLG5GRJwspUzXcS4bNqeujjZsjtdifjbMz4b52TA/G7aDa9P8vBbzs2F+NsxvJQ1X9SdoJUmSJEmSJEnX5gatJEmSJEmSJDXEDVpJkiRJkiRJaogbtJIkSZIkSZLUEDdoJUmSJEmSJKkhbtBKkiRJkiRJUkPcoJUkSZIkSZKkhrhBK0mSJEmSJEkNcYNWkiRJkiRJkhriBq0kSZIkSZIkNcQNWkmSJEmSJElqiBu0kiRJkiRJktQQN2glSZIkSZIkqSFu0EqSJEmSJElSQ4baoI2IHRFxJiLmIuKJAcf3RMSFiHi9uj1e/1C1EseOHWPLli0A99swp27DqakpgDv7j9tw/NmwHZxP87NhfjbMz4b5ua7Jz4bt4Hyanw0FQ2zQRsQa4CngYWArsDsitg546uFSyier2zM1j1MrsLi4yL59+zh69CjALDZMp7fh6dOnAdbaMBcbtoPzaX42zM+G+dkwP9c1+dmwHZxP87Ohuob5CdqHgLlSytullMvA88Cu1R2W6nTixAmmpqbYvHkzQMGG6fQ2nJycBLiEDVOxYTs4n+Znw/xsmJ8N83Ndk58N28H5ND8bqmuYDdr1wLme++erx/p9MSLeiIgXImJDLaNTLebn59mw4aokNkxmQMPL2DAVG7aD82l+NszPhvnZMD/XNfnZsB2cT/Ozobrq+iVh3wc2llIeAH4IPDfoSRGxNyJmImLmwoULNb20amLD/GyY31ANwY5jzmsxPxvmZ8P8bJifDfNzbdoOXov52fAWMMwG7TzQuzt/d/XYFaWU90opC9XdZ4Btg05USnm6lDJdSplet27dcsarZVi/fj3nzvX+ELQNsxnQcBIbplJnw+q5dmyA82l+NszPhvnZMD/Xpvm5Nm0H59P8bKiuYTZoXwPuiYhNETEJPAoc6X1CRNzVc3cn8GZ9Q9RKPfjgg7z11lu88847AIEN0+ltePnyZYC12DAVG7aD82l+NszPhvnZMD/XNfnZsB2cT/OzobombvSEUsqHEbEfeAlYAzxbSpmNiCeBmVLKEeDLEbET+JDOh4vvWcUx6yZNTExw4MABtm/fDnAf8HUb5tLbcHFxEeCSDXOxYTs4n+Znw/xsmJ8N83Ndk58N28H5ND8bqitKKY288PT0dJmZmWnktW9lEXGylDJdx7ls2Jy6OtqwOV6L+dkwPxvmZ8P8bNgOrk3z81rMz4b52TC/lTSs65eESZIkSZIkSZJukhu0kiRJkiRJktQQN2glSZIkSZIkqSFu0EqSJEmSJElSQ9yglSRJkiRJkqSGuEErSZIkSZIkSQ1xg1aSJEmSJEmSGuIGrSRJkiRJkiQ1xA1aSZIkSZIkSWqIG7SSJEmSJEmS1BA3aCVJkiRJkiSpIW7QSpIkSZIkSVJD3KCVJEmSJEmSpIa4QStJkiRJkiRJDXGDVpIkSZIkSZIaMtQGbUTsiIgzETEXEU8MOH5bRByujr8aERtrH6lW5NixY2zZsgXgfhvm1G04NTUFcGf/cRuOPxu2g/NpfjbMz4b52TA/1zX52bAdnE/zs6FgiA3aiFgDPAU8DGwFdkfE1r6nPQb8rJQyBXwT+EbdA9XyLS4usm/fPo4ePQowiw3T6W14+vRpgLU2zMWG7eB8mp8N87NhfjbMz3VNfjZsB+fT/GyormF+gvYhYK6U8nYp5TLwPLCr7zm7gOeqr18APh8RUd8wtRInTpxgamqKzZs3AxRsmE5vw8nJSYBL2DAVG7aD82l+NszPhvnZMD/XNfnZsB2cT/OzobqG2aBdD5zruX++emzgc0opHwLvA5+oY4Baufn5eTZs2ND7kA2TGdDwMjZMxYbt4Hyanw3zs2F+NszPdU1+NmwH59P8bKiuiVG+WETsBfZWdxci4tQoX79GdwAXmx7ETfhV4GMHDx58F9iykhPZsDG9DQHuW+6JWtQQcnWsrSG0qmOmhuB8OogNO2w4OjZcyoYdNhwd16aDZero2nSwTA3B+XQQG3bYsBnLbjjMBu080Ludf3f12KDnnI+ICeDjwHv9JyqlPA08DRARM6WU6eUMumnZxh4RnwW+VkrZHhEz2DDd2HsbVvfPc4s3hFzjr7MhtKdjtrE7ny6Vbew2XCrb2G24VLax23CpbGN3bTpYpvG7Nh0s29idT5fKNnYbLpV97Mv93mE+4uA14J6I2BQRk8CjwJG+5xwB/rD6+kvA35dSynIHpdpdaQgENsyo/zpciw2zsWE7OJ/mZ8P8bJifDfNzXZOfDdvB+TQ/GwoYYoO2+nyL/cBLwJvAd0spsxHxZETsrJ52EPhERMwBfwY8sVoD1s3ra3gfNkxnwHV4yYa52LAdnE/zs2F+NszPhvm5rsnPhu3gfJqfDdUVTW26R8Te6sev03Hs9Z9r1DKPHeobv38OzfFa7HDs9Z9r1Bx7/ecaNcde/7lGzbHXf65Ryzx2cG3alXn8Xosdjr3+c42aY6//XKN2q469sQ1aSZIkSZIkSbrV3fAjDiLi2Yj4aVzjt79Fx19FxFxEvBERn6p/mFoJG7aDHfOzYX42zM+G+dmwHeyYnw3zs2F+NszPhuoa5peEHQJ2XOf4w8A91W0v8Ne9ByNiR0Scqf5jWvI5GRFxW0Qcro6/GhEbhx79Khti7Hsi4kJEvF7dHm9inIP0XeSH6GvYe5EDbwPbsOE4N4S+jv0TNZ3PrRl4LdqwGTbssOFV57PjiNnwIza88nwbNsD3xA4bXnmuDRtgww4bXnU+O46YDT/Soob9x69qGMNuqpdSbngDNgKnrnHs28DunvtngLuqr9cA/wJsBiaBfwS29n3/nwLfqr5+FDg8zJhW+zbk2PcAB5oe6zXG/xvAp7rd+hsCXwCOAgF8D5iz4Xjd+hv2d+xr+Bng3wddiza0oQ2bb3gTfxZ2tKENbdi6hnV2tKENbWhDG/qemLmjDdvZsO94f8NXhznvMD9BeyPrgXM9989XjwE8RGfj7+1SymXgeWBX3/fvAp6rvn4B+HxERA3jWqlhxj62Sik/Ai5d5ym7gL8pnf96fgmYjIi7qmM2HAM307CU8g/A7cDPe453O9qwITYEbOh82jAbXmHDDhs2xPdEwIY2bJgNARv6ntgwG15xKzX8lZ79tmsa6peEVT8C/WIp5f4Bx14E/rKU8kp1/2XgK6WUmYj4ErCjlPJ4dewg8NvAv95+++3b7r333hu+tuqxsLDA3NwcH3zwwcVSyrrebtXXdwJ/UnWz4Zjq7Qi8ytXX3kU63Q5W918GvkLnf42z4ZioqeHvA38M/DKAHUdrOQ2dT8eLDfOzYTssLCxw6tSpxVLKxIC/U7iuScC1aX6uTfPzPTE/G7bHyZMnBzW80ux63ztRw+vPAxt67t9dPTbIceC/Sin7p6eny8zMdcemGp09e5ZHHnmE2dnZdwccnqfzJttlwzF1g44LwK/13O923Nj3vOPYsDE1NQSYLaXsB7DjaC2z4SDH8VpshA3zs2E7nD17lk2bNv3iGodd1yTg2jQ/16b5+Z6Ynw3bIyIGNRxKHR9xcAT4g+pDcD8DvF9K+Ul17GY2bzVavW2O0HmTnbdhKv1tCvCbA65FG44vG+Y3bMNBz7XjeLBhfjZsB98T87NhfjbMz/fE/GyY37K63PAnaCPiO8DngDsi4jzwF3Q+s5RSyreAH9D5ANw5Op+L8Uc93/4acE9EbKoG8yjwuzf+d1Gddu/ezfHjx7l48SLAAxHxGPAL4KsR8TzwHvCfwCvYcGz1d6TT66sR8XHgdeDHwJssvRZtOCZsmN8KGoIdx4IN87NhO3Q7ArdVf8d4Ad8TU3Fdk58N8/M9MT8bttIRYH+13/Zprt5Uv7ay+r/d7AvAP9P57WxfrR57ctu2bUWjB7zb+QcBPFV1+SdgutgwDWDGhrnV1RDYacdm3GzD4rU4dmyYnw3zc23aDq5N83Ntmp/vifnZML/lNOzehvolYavBz8NoRkScLKVM13EuGzanro42bI7XYn42zM+G+dkwPxu2g2vT/LwW87NhfjbMbyUN6/gMWkmSJEmSJEnSMrhBK0mSJEmSJEkNcYNWkiRJkiRJkhriBq0kSZIkSZIkNcQNWkmSJEmSJElqiBu0kiRJkiRJktQQN2glSZIkSZIkqSFu0EqSJEmSJElSQ9yglSRJkiRJkqSGuEErSZIkSZIkSQ1xg1aSJEmSJEmSGuIGrSRJkiRJkiQ1xA1aSZIkSZIkSWqIG7SSJEmSJEmS1JChNmgjYkdEnImIuYh4YsDxPRFxISJer26P1z9UrcSxY8fYsmULwP02zKnbcGpqCuDO/uM2HH82bAfn0/xsmJ8N87Nhfq5r8rNhOzif5mdDwRAbtBGxBngKeBjYCuyOiK0Dnnq4lPLJ6vZMzePUCiwuLrJv3z6OHj0KMIsN0+ltePr0aYC1NszFhu3gfJqfDfOzYX42zM91TX42bAfn0/xsqK5hfoL2IWCulPJ2KeUy8Dywa3WHpTqdOHGCqakpNm/eDFCwYTq9DScnJwEuYcNUbNgOzqf52TA/G+Znw/xc1+Rnw3ZwPs3PhuoaZoN2PXCu5/756rF+X4yINyLihYjYUMvoVIv5+Xk2bLgqiQ2TGdDwMjZMxYbt4Hyanw3zs2F+NszPdU1+NmwH59P8bKiuun5J2PeBjaWUB4AfAs8NelJE7I2ImYiYuXDhQk0vrZrYMD8b5jdUQ7DjmPNazM+G+dkwPxvmZ8P8XJu2g9difja8BQyzQTsP9O7O3109dkUp5b1SykJ19xlg26ATlVKeLqVMl1Km161bt5zxahnWr1/PuXO9PwRtw2wGNJzEhqnU2bB6rh0b4Hyanw3zs2F+NszPtWl+rk3bwfk0Pxuqa5gN2teAeyJiU0RMAo8CR3qfEBF39dzdCbxZ3xC1Ug8++CBvvfUW77zzDkBgw3R6G16+fBlgLTZMxYbt4Hyanw3zs2F+NszPdU1+NmwH59P8bKiuiRs9oZTyYUTsB14C1gDPllJmI+JJYKaUcgT4ckTsBD6k8+Hie1ZxzLpJExMTHDhwgO3btwPcB3zdhrn0NlxcXAS4ZMNcbNgOzqf52TA/G+Znw/xc1+Rnw3ZwPs3PhuqKUkojLzw9PV1mZmYaee1bWUScLKVM13EuGzanro42bI7XYn42zM+G+dkwPxu2g2vT/LwW87NhfjbMbyUN6/olYZIkSZIkSZKkm+QGrSRJkiRJkiQ1xA1aSZIkSZIkSWqIG7SSJEmSJEmS1BA3aCVJkiRJkiSpIW7QSpIkSZIkSVJD3KCVJEmSJEmSpIa4QStJkiRJkiRJDXGDVpIkSZIkSZIa4gatJEmSJEmSJDXEDVpJkiRJkiRJaogbtJIkSZIkSZLUEDdoJUmSJEmSJKkhbtBKkiRJkiRJUkPcoJUkSZIkSZKkhrhBK0mSJEmSJEkNGWqDNiJ2RMSZiJiLiCcGHL8tIg5Xx1+NiI21j1QrcuzYMbZs2QJwvw1z6jacmpoCuLP/uA3Hnw3bwfk0PxvmZ8P8bJif65r8bNgOzqf52VAwxAZtRKwBngIeBrYCuyNia9/THgN+VkqZAr4JfKPugWr5FhcX2bdvH0ePHgWYxYbp9DY8ffo0wFob5mLDdnA+zc+G+dkwPxvm57omPxu2g/NpfjZU1zA/QfsQMFdKebuUchl4HtjV95xdwHPV1y8An4+IqG+YWokTJ04wNTXF5s2bAQo2TKe34eTkJMAlbJiKDdvB+TQ/G+Znw/xsmJ/rmvxs2A7Op/nZUF3DbNCuB8713D9fPTbwOaWUD4H3gU/UMUCt3Pz8PBs2bOh9yIbJDGh4GRumYsN2cD7Nz4b52TA/G+bnuiY/G7aD82l+NlTXxChfLCL2AnuruwsRcWqUr1+jO4CLTQ/iJvwq8LGDBw++C2xZyYls2JjehgD3LfdELWoIuTrW1hBa1TFTQ3A+HcSGHTYcHRsuZcMOG46Oa9PBMnV0bTpYpobgfDqIDTts2IxlNxxmg3Ye6N3Ov7t6bNBzzkfEBPBx4L3+E5VSngaeBoiImVLK9HIG3bRsY4+IzwJfK6Vsj4gZbJhu7L0Nq/vnucUbQq7x19kQ2tMx29idT5fKNnYbLpVt7DZcKtvYbbhUtrG7Nh0s0/hdmw6WbezOp0tlG7sNl8o+9uV+7zAfcfAacE9EbIqISeBR4Ejfc44Af1h9/SXg70spZbmDUu2uNAQCG2bUfx2uxYbZ2LAdnE/zs2F+NszPhvm5rsnPhu3gfJqfDQUMsUFbfb7FfuAl4E3gu6WU2Yh4MiJ2Vk87CHwiIuaAPwOeWK0B6+b1NbwPG6Yz4Dq8ZMNcbNgOzqf52TA/G+Znw/xc1+Rnw3ZwPs3PhuqKpjbdI2Jv9ePX6Tj2+s81apnHDvWN3z+H5ngtdjj2+s81ao69/nONmmOv/1yj5tjrP9eoZR47uDbtyjx+r8UOx17/uUbNsdd/rlG7Vcd+ww3aiHgWeAT4aSnl/gHHA/ifwBeAnwN7Sin/ZzmD0eqwYTvYMT8b5mfD/GyYnw3bwY752TA/G+Znw/xsqK5hPoP2ELDjOscfBu6pbnuBv175sFSzQ9iwDQ5hx+wOYcPsDmHD7A5hw+wOYcM2OIQdszuEDbM7hA2zO4QNszuEDcVwn0H7I+DSdZ6yC/ib0vEPwK9ExF3dgxGxIyLORMRcRCz5nIyIuC0iDlfHX42Ijcv491gVQ4x9T0RciIjXq9vjTYxzkIh4NiJ+GhGnBjWMjr+qPsPkfwM/suH4NoSl12Jvw4h4A3iMa1yLNmyGV41M1AAAVERJREFUDTts6HzaJBt+xIZXnm/DBvie2GFDGzbJhh029D2xSTb8SFsaDjh+VcOI+NRQJy6l3PAGbAROXePYi8D/6Ln/MjBdfb0G+BdgMzAJ/COwte/7/xT4VvX1o8DhYca02rchx74HOND0WK8x/t8APtXt1t+Qzo/HHwUCeAWYteF43fob9nfsa/gZ4GeDrkUb2tCGzTe8iT8LO9rQhjZsXcM6O9rQhja0oQ19T8zc0YbtbNh3vL/hq8Ocd6hfElbtsL9YBn8exovAX5ZSXqnuvwx8pZQyExGfBb5WStleHftb4CHg326//fZt99577w1fW/VYWFhgbm6ODz744GIpZV1EfBs4Xkr5TtXwAeDTpZSf2HB89XYE/paqIUBE/Afwe6WUv6vuvwx8BfglbDg2amr458CDwK8D2HG0ltPQ+XS82DA/G7bDwsICp06dWiylTPSuTcF1TRauTfNzbZqf74n52bA9Tp48OajhGeBzpZSfXO97J2p4/XlgQ8/9u6vHANYD53qOfQ/4cSll//T0dJmZmanh5TWMs2fP8sgjjzA7O/tu9VBvm3k6/4vNeuAn2HBs9XXsb/M+cB/wd9X9bsf/jg3HRk0NzwPrSym/A2DH0VpmQwY812uxITbMz4btcPbsWTZt2vSL6q7rmoRcm+bn2jQ/3xPzs2F7RMSghuf5aL/tmob5JWE3cgT4g+ozFj4DvH+jXWGNnSPAnQA2TO0C8Ftei6nZMD8b5mfD/GzYDnbMz4b52TA/G+Znw1vEDX+CNiK+A3wOuCMizgN/Qef/0kAp5VvAD+h8vsIc8HPgj3q+/Xo/XasR2b17N8ePH+fixYsAD0TEY8DH6Hyexyt0Gq4BXgD+HzYcS/0dgf8A9kTE/dW1+N+A/8vSa9GGY8KG+a2gIdhxLNgwPxu2Q7cjcFv1d4w5fE9MxXVNfjbMz/fE/GzYSsvrssofnDsBvA1s4qMP/b2vlMK2bduKRg+Y6fyD3+LqDy0+UWyYBjBjw9zqaljs2JibaVi8FseSDfOzYX6uTdvBtWl+rk3z8z0xPxvmd7MNe291fMTBNZVSPgT2Ay8BbwLfLaXMRsSTq/m6uq511T9/QOdCngP+F53f7LeEDceaDfNbccOI2DmqwWqgoRqC1+IYs2F+NszNtWl72DA/16b5+Z6Ynw3zG7phr+hs8I6eH1jcjIg4WUqZruNcNmxOXR1t2ByvxfxsmJ8N87NhfjZsB9em+Xkt5mfD/GyY30oarupP0EqSJEmSJEmSrs0NWkmSJEmSJElqiBu0kiRJkiRJktQQN2glSZIkSZIkqSFu0EqSJEmSJElSQ9yglSRJkiRJkqSGuEErSZIkSZIkSQ1xg1aSJEmSJEmSGuIGrSRJkiRJkiQ1xA1aSZIkSZIkSWqIG7SSJEmSJEmS1BA3aCVJkiRJkiSpIW7QSpIkSZIkSVJD3KCVJEmSJEmSpIYMtUEbETsi4kxEzEXEEwOO74mICxHxenV7vP6haiWOHTvGli1bAO63YU7dhlNTUwB39h+34fizYTs4n+Znw/xsmJ8N83Ndk58N28H5ND8bCobYoI2INcBTwMPAVmB3RGwd8NTDpZRPVrdnah6nVmBxcZF9+/Zx9OhRgFlsmE5vw9OnTwOstWEuNmwH59P8bJifDfOzYX6ua/KzYTs4n+ZnQ3UN8xO0DwFzpZS3SymXgeeBXas7LNXpxIkTTE1NsXnzZoCCDdPpbTg5OQlwCRumYsN2cD7Nz4b52TA/G+bnuiY/G7aD82l+NlTXMBu064FzPffPV4/1+2JEvBERL0TEhlpGp1rMz8+zYcNVSWyYzICGl7FhKjZsB+fT/GyYnw3zs2F+rmvys2E7OJ/mZ0N11fVLwr4PbCylPAD8EHhu0JMiYm9EzETEzIULF2p6adXEhvnZML+hGoIdx5zXYn42zM+G+dkwPxvm59q0HbwW87PhLWCYDdp5oHd3/u7qsStKKe+VUhaqu88A2wadqJTydCllupQyvW7duuWMV8uwfv16zp3r/SFoG2YzoOEkNkylzobVc+3YAOfT/GyYnw3zs2F+rk3zc23aDs6n+dlQXcNs0L4G3BMRmyJiEngUONL7hIi4q+fuTuDN+oaolXrwwQd56623eOeddwACG6bT2/Dy5csAa7FhKjZsB+fT/GyYnw3zs2F+rmvys2E7OJ/mZ0N1TdzoCaWUDyNiP/ASsAZ4tpQyGxFPAjOllCPAlyNiJ/AhnQ8X37OKY9ZNmpiY4MCBA2zfvh3gPuDrNsylt+Hi4iLAJRvmYsN2cD7Nz4b52TA/G+bnuiY/G7aD82l+NlRXlFIaeeHp6ekyMzPTyGvfyiLiZClluo5z2bA5dXW0YXO8FvOzYX42zM+G+dmwHVyb5ue1mJ8N87NhfitpWNcvCZMkSZIkSZIk3SQ3aCVJkiRJkiSpIW7QSpIkSZIkSVJD3KCVJEmSJEmSpIa4QStJkiRJkiRJDXGDVpIkSZIkSZIa4gatJEmSJEmSJDXEDVpJkiRJkiRJaogbtJIkSZIkSZLUEDdoJUmSJEmSJKkhbtBKkiRJkiRJUkPcoJUkSZIkSZKkhrhBK0mSJEmSJEkNcYNWkiRJkiRJkhriBq0kSZIkSZIkNWSoDdqI2BERZyJiLiKeGHD8tog4XB1/NSI21j5SrcixY8fYsmULwP02zKnbcGpqCuDO/uM2HH82bAfn0/xsmJ8N87Nhfq5r8rNhOzif5mdDwRAbtBGxBngKeBjYCuyOiK19T3sM+FkpZQr4JvCNugeq5VtcXGTfvn0cPXoUYBYbptPb8PTp0wBrbZiLDdvB+TQ/G+Znw/xsmJ/rmvxs2A7Op/nZUF3D/ATtQ8BcKeXtUspl4HlgV99zdgHPVV+/AHw+IqK+YWolTpw4wdTUFJs3bwYo2DCd3oaTk5MAl7BhKjZsB+fT/GyYnw3zs2F+rmvys2E7OJ/+/+3dUYxc5Z2w+ecv9zS7QhMSAxqQ8cq2GhkZhEa4IUS7iiJlJAwfY0sbLuxZTeIZkBXFVi5yE1ZcTMRc7MzVaCPQMCxGkO8iOIu+0eeg2CginxVxEUxbS4jbiKEDZuye7MTGEdpvZmiH1rsXdcqUq8t2uft0nfofPz+plK46p6tf/HDeev3SqcrPhuoaZoN2HXCq5/7p6rGB55RSPgU+Bm6sY4Baufn5edavX9/7kA2TGdDwPDZMxYbt4Hyanw3zs2F+NszPdU1+NmwH59P8bKiuiVH+sIjYA+yp7i5ExPFR/vwa3QScbXoQV+ELwOf279//IbB5JU9kw8b0NgS4c7lP1KKGkKtjbQ2hVR0zNQTn00Fs2GHD0bHhUjbssOHouDYdLFNH16aDZWoIzqeD2LDDhs1YdsNhNmjngd7t/NuqxwadczoiJoAbgI/6n6iU8izwLEBEzJRSppcz6KZlG3tEfAn4XinlgYiYwYbpxt7bsLp/mmu8IeQaf50NoT0ds43d+XSpbGO34VLZxm7DpbKN3YZLZRu7a9PBMo3ftelg2cbufLpUtrHbcKnsY1/u9w7zFgdvArdHxMaImAR2Agf7zjkIfKP6+hHgZ6WUstxBqXYXGgKBDTPqvw7XYsNsbNgOzqf52TA/G+Znw/xc1+Rnw3ZwPs3PhgKG2KCt3t9iH/Aq8A7wo1LKbEQ8GRHbq9P2AzdGxBzwHeDx1Rqwrl5fwzuxYToDrsNzNszFhu3gfJqfDfOzYX42zM91TX42bAfn0/xsqK5oatM9IvZUv36djmOv/7lGLfPYob7x++fQHK/FDsde/3ONmmOv/7lGzbHX/1yj5tjrf65Ryzx2cG3alXn8Xosdjr3+5xo1x17/c43atTr2xjZoJUmSJEmSJOlad8W3OIiI5yPit3GJT3+Lju9HxFxEvB0R99Q/TK2EDdvBjvnZMD8b5mfD/GzYDnbMz4b52TA/G+ZnQ3UN8yFhLwDbLnP8QeD26rYH+PvegxGxLSLerf5lWvI+GRFxXUQcqI6/EREbhh79Khti7Lsj4kxEvFXdHmtinIP0XeQv0New9yIH3ge2YsNxbgh9HfsnajrvWzPwWrRhM2zYYcOLns+OI2bDz9jwwvk2bICviR02vHCuDRtgww4bXvR8dhwxG36mRQ37j1/UMIbdVC+lXPEGbACOX+LYPwC7eu6/C9xafb0G+DWwCZgEfgls6fv+bwHPVF/vBA4MM6bVvg059t3AU02P9RLj/zJwT7dbf0PgIeAQEMA/AnM2HK9bf8P+jn0N7wf+ddC1aEMb2rD5hlfxZ2FHG9rQhq1rWGdHG9rQhja0oa+JmTvasJ0N+473N3xjmOcd5jdor2QdcKrn/unqMYD76Gz8vV9KOQ+8BOzo+/4dwIvV1y8DX42IqGFcKzXM2MdWKeXnwLnLnLID+EHp/NvzB8BkRNxaHbPhGLiahqWUXwDXA//ec7zb0YYNsSFgQ+fThtnwAht22LAhviYCNrRhw2wI2NDXxIbZ8IJrqeHne/bbLmmoDwmrfgX6lVLKXQOOvQL8TSnl9er+a8B3SykzEfEIsK2U8lh1bD/wp8A/X3/99VvvuOOOK/5s1WNhYYG5uTk++eSTs6WUm3u7VV/fAnyz6mbDMdXbEXiDi6+9s3S67a/uvwZ8l85/jbPhmKip4Z8Dfwn8IYAdR2s5DZ1Px4sN87NhOywsLHD8+PHFUsrEgL9TuK5JwLVpfq5N8/M1MT8btsexY8cGNbzQ7HLfO1HDz58H1vfcv616bJAjwH+UUvZNT0+XmZnLjk01OnnyJA8//DCzs7MfDjg8T+dFtsuGY+oKHReAP+q53+24oe+8I9iwMTU1BJgtpewDsONoLbPhIEfwWmyEDfOzYTucPHmSjRs3/v4Sh13XJODaND/Xpvn5mpifDdsjIgY1HEodb3FwEPh69Sa49wMfl1J+Ux27ms1bjVZvm4N0XmTnbZhKf5sC/MmAa9GG48uG+Q3bcNC5dhwPNszPhu3ga2J+NszPhvn5mpifDfNbVpcr/gZtRPwQ+ApwU0ScBv6KznuWUkp5BvgJnTfAnaPzvhh/0fPtbwK3R8TGajA7gT+78j+L6rRr1y6OHDnC2bNnAe6OiEeB3wNPRMRLwEfAvwGvY8Ox1d+RTq8nIuIG4C3gX4B3WHot2nBM2DC/FTQEO44FG+Znw3bodgSuq/6O8TK+JqbiuiY/G+bna2J+Nmylg8C+ar/ti1y8qX5pZfU/3ewh4J/ofDrbE9VjT27durVo9IAPO/9DAE9XXX4FTBcbpgHM2DC3uhoC2+3YjKttWLwWx44N87Nhfq5N28G1aX6uTfPzNTE/G+a3nIbd21AfErYafD+MZkTEsVLKdB3PZcPm1NXRhs3xWszPhvnZMD8b5mfDdnBtmp/XYn42zM+G+a2kYR3vQStJkiRJkiRJWgY3aCVJkiRJkiSpIW7QSpIkSZIkSVJD3KCVJEmSJEmSpIa4QStJkiRJkiRJDXGDVpIkSZIkSZIa4gatJEmSJEmSJDXEDVpJkiRJkiRJaogbtJIkSZIkSZLUEDdoJUmSJEmSJKkhbtBKkiRJkiRJUkPcoJUkSZIkSZKkhrhBK0mSJEmSJEkNcYNWkiRJkiRJkhoy1AZtRGyLiHcjYi4iHh9wfHdEnImIt6rbY/UPVStx+PBhNm/eDHCXDXPqNpyamgK4pf+4DcefDdvB+TQ/G+Znw/xsmJ/rmvxs2A7Op/nZUDDEBm1ErAGeBh4EtgC7ImLLgFMPlFL+uLo9V/M4tQKLi4vs3buXQ4cOAcxiw3R6G544cQJgrQ1zsWE7OJ/mZ8P8bJifDfNzXZOfDdvB+TQ/G6prmN+gvQ+YK6W8X0o5D7wE7FjdYalOR48eZWpqik2bNgEUbJhOb8PJyUmAc9gwFRu2g/NpfjbMz4b52TA/1zX52bAdnE/zs6G6htmgXQec6rl/unqs39ci4u2IeDki1tcyOtVifn6e9esvSmLDZAY0PI8NU7FhOzif5mfD/GyYnw3zc12Tnw3bwfk0Pxuqq64PCfsxsKGUcjfwU+DFQSdFxJ6ImImImTNnztT0o1UTG+Znw/yGagh2HHNei/nZMD8b5mfD/GyYn2vTdvBazM+G14BhNmjngd7d+duqxy4opXxUSlmo7j4HbB30RKWUZ0sp06WU6Ztvvnk549UyrFu3jlOnen8J2obZDGg4iQ1TqbNhda4dG+B8mp8N87NhfjbMz7Vpfq5N28H5ND8bqmuYDdo3gdsjYmNETAI7gYO9J0TErT13twPv1DdErdS9997Le++9xwcffAAQ2DCd3obnz58HWIsNU7FhOzif5mfD/GyYnw3zc12Tnw3bwfk0Pxuqa+JKJ5RSPo2IfcCrwBrg+VLKbEQ8CcyUUg4C346I7cCndN5cfPcqjllXaWJigqeeeooHHngA4E7gr22YS2/DxcVFgHM2zMWG7eB8mp8N87NhfjbMz3VNfjZsB+fT/GyoriilNPKDp6eny8zMTCM/+1oWEcdKKdN1PJcNm1NXRxs2x2sxPxvmZ8P8bJifDdvBtWl+Xov52TA/G+a3koZ1fUiYJEmSJEmSJOkquUErSZIkSZIkSQ1xg1aSJEmSJEmSGuIGrSRJkiRJkiQ1xA1aSZIkSZIkSWqIG7SSJEmSJEmS1BA3aCVJkiRJkiSpIW7QSpIkSZIkSVJD3KCVJEmSJEmSpIa4QStJkiRJkiRJDXGDVpIkSZIkSZIa4gatJEmSJEmSJDXEDVpJkiRJkiRJaogbtJIkSZIkSZLUEDdoJUmSJEmSJKkhbtBKkiRJkiRJUkOG2qCNiG0R8W5EzEXE4wOOXxcRB6rjb0TEhtpHqhU5fPgwmzdvBrjLhjl1G05NTQHc0n/chuPPhu3gfJqfDfOzYX42zM91TX42bAfn0/xsKBhigzYi1gBPAw8CW4BdEbGl77RHgd+VUqaAvwP+tu6BavkWFxfZu3cvhw4dApjFhun0Njxx4gTAWhvmYsN2cD7Nz4b52TA/G+bnuiY/G7aD82l+NlTXML9Bex8wV0p5v5RyHngJ2NF3zg7gxerrl4GvRkTUN0ytxNGjR5mammLTpk0ABRum09twcnIS4Bw2TMWG7eB8mp8N87NhfjbMz3VNfjZsB+fT/GyorokhzlkHnOq5fxr44qXOKaV8GhEfAzcCZ3tPiog9wJ7q7kJEHF/OoMfATfT9s425LwCfi4gPgc3YEHI3BLiTTrNe11pDyNWxtobQqo6ZGoLz6SA27LDh6NhwKRt22HB0XJsOlqmja9PBMjUE59NBbNhhw2ZsXu43DrNBW5tSyrPAswARMVNKmR7lz69LtrFHxCPAtlLKYxExs5LnsmEzehtW999f7nO1pSHkGn+dDaE9HbON3fl0qWxjt+FS2cZuw6Wyjd2GS2Ubu2vTwTKN37XpYNnG7ny6VLax23Cp7GNf7vcO8xYH88D6nvu3VY8NPCciJoAbgI+WOyjVzob59TecxIbZ2LAdnE/zs2F+NszPhvm5rsnPhu3gfJqfDQUMt0H7JnB7RGyMiElgJ3Cw75yDwDeqrx8BflZKKfUNUyt0oSEQ2DCj/utwLTbMxobt4Hyanw3zs2F+NszPdU1+NmwH59P8bChgiLc4qN7fYh/wKrAGeL6UMhsRTwIzpZSDwH7gP0fEHJ03F985xM9+dgXjblqqsfc1/Dzwf9ow19gHXIev2hBINP5VbAiJ/hwGSDV259OBUo3dhgOlGrsNB0o1dhsOlGrsrk0vKc34XZteUqqxO58OlGrsNhzomhx7uOkuSZIkSZIkSc244lscRMTzEfHbS336W3R8PyLmIuLtiLin/mFqJWzYDnbMz4b52TA/G+Znw3awY342zM+G+dkwPxuqa5j3oH0B2HaZ4w8Ct1e3PcDfr3xYqtkL2LANXsCO2b2ADbN7ARtm9wI2zO4FbNgGL2DH7F7Ahtm9gA2zewEbZvcCNhRDbNCWUn5O5z0uLmUH8IPS8Qvg8xFxa/dgRGyLiHer3f7H+785Iq6LiAPV8TciYsMy/jlWxRBj3x0RZyLirer2WBPjHKT3v8IMatj7X2GA/wz83Ibj2xCWXov9/yUNeJRLXIs2bIYNO2zofNokG37GhhfOt2EDfE3ssKENm2TDDhv6mtgkG36mLQ0HHL+oYQz7W8+llCvegA3A8UscewX4X3ruvwZMV1+vAX4NbAImgV8CW/q+/1vAM9XXO4EDw4xptW9Djn038FTTY73E+L8M3NPt1t8QeAg4ROdTAl8HZm04Xrf+hv0d+xreD/xu0LVoQxvasPmGV/FnYUcb2tCGrWtYZ0cb2tCGNrShr4mZO9qwnQ37jvc3fGOY5x3qQ8KqHfZXSil3DTj2CvA3pZTXq/uvAd8tpcxExJeA75VSHqiO/RfgPuD/vf7667fecccdV/zZqsfCwgJzc3N88sknZ0spN0fEPwBHSik/rBreDXyxlPIbG46v3o7Af6FqCBAR/x3430op/7W6/xrwXeAPsOHYqKnh/w7cC/xPAHYcreU0dD4dLzbMz4btsLCwwPHjxxdLKRO9a1NwXZOFa9P8XJvm52tifjZsj2PHjg1q+C7wlVLKby73vRM1/Px5YH3P/duqxwDWAad6jv0j8C+llH3T09NlZmamhh+vYZw8eZKHH36Y2dnZD6uHetvM0/kvNuuA32DDsdXXsb/Nx8CdwH+t7nc7/s/YcGzU1PA0sK6U8r8C2HG0ltmQAed6LTbEhvnZsB1OnjzJxo0bf1/ddV2TkGvT/Fyb5udrYn42bI+IGNTwNJ/tt13SMB8SdiUHga9X77FwP/DxlXaFNXYOArcA2DC1M8B/8lpMzYb52TA/G+Znw3awY342zM+G+dkwPxteI674G7QR8UPgK8BNEXEa+Cs6/5cGSinPAD+h8/4Kc8C/A3/R8+2X++1ajciuXbs4cuQIZ8+eBbg7Ih4FPkfn/Txep9NwDfAy8P9hw7HU3xH478DuiLiruhb/R+D/Yem1aMMxYcP8VtAQ7DgWbJifDduh2xG4rvo7xhy+JqbiuiY/G+bna2J+Nmyl5XVZ5TfOnQDeBzby2Zv+3llKYevWrUWjB8x0/of/xMVvWny02DANYMaGudXVsNixMVfTsHgtjiUb5mfD/FybtoNr0/xcm+bna2J+Nszvahv23up4i4NLKqV8CuwDXgXeAX5USpmNiCdX8+fqsm6u/vcndC7kOeD/ovPJfkvYcKzZML8VN4yI7aMarAYaqiF4LY4xG+Znw9xcm7aHDfNzbZqfr4n52TC/oRv2is4G7+j5hsXNiIhjpZTpOp7Lhs2pq6MNm+O1mJ8N87NhfjbMz4bt4No0P6/F/GyYnw3zW0nDVf0NWkmSJEmSJEnSpblBK0mSJEmSJEkNcYNWkiRJkiRJkhriBq0kSZIkSZIkNcQNWkmSJEmSJElqiBu0kiRJkiRJktQQN2glSZIkSZIkqSFu0EqSJEmSJElSQ9yglSRJkiRJkqSGuEErSZIkSZIkSQ1xg1aSJEmSJEmSGuIGrSRJkiRJkiQ1xA1aSZIkSZIkSWqIG7SSJEmSJEmS1JChNmgjYltEvBsRcxHx+IDjuyPiTES8Vd0eq3+oWonDhw+zefNmgLtsmFO34dTUFMAt/cdtOP5s2A7Op/nZMD8b5mfD/FzX5GfDdnA+zc+GgiE2aCNiDfA08CCwBdgVEVsGnHqglPLH1e25msepFVhcXGTv3r0cOnQIYBYbptPb8MSJEwBrbZiLDdvB+TQ/G+Znw/xsmJ/rmvxs2A7Op/nZUF3D/AbtfcBcKeX9Usp54CVgx+oOS3U6evQoU1NTbNq0CaBgw3R6G05OTgKcw4ap2LAdnE/zs2F+NszPhvm5rsnPhu3gfJqfDdU1zAbtOuBUz/3T1WP9vhYRb0fEyxGxvpbRqRbz8/OsX39REhsmM6DheWyYig3bwfk0PxvmZ8P8bJif65r8bNgOzqf52VBddX1I2I+BDaWUu4GfAi8OOiki9kTETETMnDlzpqYfrZrYMD8b5jdUQ7DjmPNazM+G+dkwPxvmZ8P8XJu2g9difja8BgyzQTsP9O7O31Y9dkEp5aNSykJ19zlg66AnKqU8W0qZLqVM33zzzcsZr5Zh3bp1nDrV+0vQNsxmQMNJbJhKnQ2rc+3YAOfT/GyYnw3zs2F+rk3zc23aDs6n+dlQXcNs0L4J3B4RGyNiEtgJHOw9ISJu7bm7HXinviFqpe69917ee+89PvjgA4DAhun0Njx//jzAWmyYig3bwfk0PxvmZ8P8bJif65r8bNgOzqf52VBdE1c6oZTyaUTsA14F1gDPl1JmI+JJYKaUchD4dkRsBz6l8+biu1dxzLpKExMTPPXUUzzwwAMAdwJ/bcNcehsuLi4CnLNhLjZsB+fT/GyYnw3zs2F+rmvys2E7OJ/mZ0N1RSmlkR88PT1dZmZmGvnZ17KIOFZKma7juWzYnLo62rA5Xov52TA/G+Znw/xs2A6uTfPzWszPhvnZML+VNKzrQ8IkSZIkSZIkSVfJDVpJkiRJkiRJaogbtJIkSZIkSZLUEDdoJUmSJEmSJKkhbtBKkiRJkiRJUkPcoJUkSZIkSZKkhrhBK0mSJEmSJEkNcYNWkiRJkiRJkhriBq0kSZIkSZIkNcQNWkmSJEmSJElqiBu0kiRJkiRJktQQN2glSZIkSZIkqSFu0EqSJEmSJElSQ9yglSRJkiRJkqSGuEErSZIkSZIkSQ0ZaoM2IrZFxLsRMRcRjw84fl1EHKiOvxERG2ofqVbk8OHDbN68GeAuG+bUbTg1NQVwS/9xG44/G7aD82l+NszPhvnZMD/XNfnZsB2cT/OzoWCIDdqIWAM8DTwIbAF2RcSWvtMeBX5XSpkC/g7427oHquVbXFxk7969HDp0CGAWG6bT2/DEiRMAa22Yiw3bwfk0PxvmZ8P8bJif65r8bNgOzqf52VBdw/wG7X3AXCnl/VLKeeAlYEffOTuAF6uvXwa+GhFR3zC1EkePHmVqaopNmzYBFGyYTm/DyclJgHPYMBUbtoPzaX42zM+G+dkwP9c1+dmwHZxP87OhuobZoF0HnOq5f7p6bOA5pZRPgY+BG+sYoFZufn6e9evX9z5kw2QGNDyPDVOxYTs4n+Znw/xsmJ8N83Ndk58N28H5ND8bqmtilD8sIvYAe6q7CxFxfJQ/v0Y3AWebHsRV+ALwuf37938IbF7JE9mwMb0NAe5c7hO1qCHk6lhbQ2hVx0wNwfl0EBt22HB0bLiUDTtsODquTQfL1NG16WCZGoLz6SA27LBhM5bdcJgN2nmgdzv/tuqxQeecjogJ4Abgo/4nKqU8CzwLEBEzpZTp5Qy6adnGHhFfAr5XSnkgImawYbqx9zas7p/mGm8IucZfZ0NoT8dsY3c+XSrb2G24VLax23CpbGO34VLZxu7adLBM43dtOli2sTufLpVt7DZcKvvYl/u9w7zFwZvA7RGxMSImgZ3Awb5zDgLfqL5+BPhZKaUsd1Cq3YWGQGDDjPqvw7XYMBsbtoPzaX42zM+G+dkwP9c1+dmwHZxP87OhgCE2aKv3t9gHvAq8A/yolDIbEU9GxPbqtP3AjRExB3wHeHy1Bqyr19fwTmyYzoDr8JwNc7FhOzif5mfD/GyYnw3zc12Tnw3bwfk0PxuqK5radI+IPdWvX6fj2Ot/rlHLPHaob/z+OTTHa7HDsdf/XKPm2Ot/rlFz7PU/16g59vqfa9Qyjx1cm3ZlHr/XYodjr/+5Rs2x1/9co3atjr2xDVpJkiRJkiRJutZd8S0OIuL5iPhtXOLT36Lj+xExFxFvR8Q99Q9TK2HDdrBjfjbMz4b52TA/G7aDHfOzYX42zM+G+dlQXcN8SNgLwLbLHH8QuL267QH+vvdgRGyLiHerf5mWvE9GRFwXEQeq429ExIahR7/Khhj77og4ExFvVbfHmhjnIH0X+Qv0Ney9yIH3ga3YcJwbQl/H/omazvvWDLwWbdgMG3bY8KLns+OI2fAzNrxwvg0b4Gtihw0vnGvDBtiww4YXPZ8dR8yGn2lRw/7jFzWMYTfVSylXvAEbgOOXOPYPwK6e++8Ct1ZfrwF+DWwCJoFfAlv6vv9bwDPV1zuBA8OMabVvQ459N/BU02O9xPi/DNzT7dbfEHgIOAQE8I/AnA3H69bfsL9jX8P7gX8ddC3a0IY2bL7hVfxZ2NGGNrRh6xrW2dGGNrShDW3oa2LmjjZsZ8O+4/0N3xjmeYf5DdorWQec6rl/unoM4D46G3/vl1LOAy8BO/q+fwfwYvX1y8BXIyJqGNdKDTP2sVVK+Tlw7jKn7AB+UDr/9vwBMBkRt1bHbDgGrqZhKeUXwPXAv/cc73a0YUNsCNjQ+bRhNrzAhh02bIiviYANbdgwGwI29DWxYTa84Fpq+Pme/bZLGupDwqpfgX6llHLXgGOvAH9TSnm9uv8a8N1SykxEPAJsK6U8Vh3bD/wp8M/XX3/91jvuuOOKP1v1WFhYYG5ujk8++eRsKeXm3m7V17cA36y62XBM9XYE3uDia+8snW77q/uvAd+l81/jbDgmamr458BfAn8IYMfRWk5D59PxYsP8bNgOCwsLHD9+fLGUMjHg7xSuaxJwbZqfa9P8fE3Mz4btcezYsUENLzS73PdO1PDz54H1Pfdvqx4b5AjwH6WUfdPT02Vm5rJjU41OnjzJww8/zOzs7IcDDs/TeZHtsuGYukLHBeCPeu53O27oO+8INmxMTQ0BZksp+wDsOFrLbDjIEbwWG2HD/GzYDidPnmTjxo2/v8Rh1zUJuDbNz7Vpfr4m5mfD9oiIQQ2HUsdbHBwEvl69Ce79wMellN9Ux65m81aj1dvmIJ0X2XkbptLfpgB/MuBatOH4smF+wzYcdK4dx4MN87NhO/iamJ8N87Nhfr4m5mfD/JbV5Yq/QRsRPwS+AtwUEaeBv6LznqWUUp4BfkLnDXDn6Lwvxl/0fPubwO0RsbEazE7gz678z6I67dq1iyNHjnD27FmAuyPiUeD3wBMR8RLwEfBvwOvYcGz1d6TT64mIuAF4C/gX4B2WXos2HBM2zG8FDcGOY8GG+dmwHbodgeuqv2O8jK+Jqbiuyc+G+fmamJ8NW+kgsK/ab/siF2+qX1pZ/U83ewj4JzqfzvZE9diTW7duLRo94MPO/xDA01WXXwHTxYZpADM2zK2uhsB2OzbjahsWr8WxY8P8bJifa9N2cG2an2vT/HxNzM+G+S2nYfc21IeErQbfD6MZEXGslDJdx3PZsDl1dbRhc7wW87NhfjbMz4b52bAdXJvm57WYnw3zs2F+K2lYx3vQSpIkSZIkSZKWwQ1aSZIkSZIkSWqIG7SSJEmSJEmS1BA3aCVJkiRJkiSpIW7QSpIkSZIkSVJD3KCVJEmSJEmSpIa4QStJkiRJkiRJDXGDVpIkSZIkSZIa4gatJEmSJEmSJDXEDVpJkiRJkiRJaogbtJIkSZIkSZLUEDdoJUmSJEmSJKkhbtBKkiRJkiRJUkPcoJUkSZIkSZKkhgy1QRsR2yLi3YiYi4jHBxzfHRFnIuKt6vZY/UPVShw+fJjNmzcD3GXDnLoNp6amAG7pP27D8WfDdnA+zc+G+dkwPxvm57omPxu2g/NpfjYUDLFBGxFrgKeBB4EtwK6I2DLg1AOllD+ubs/VPE6twOLiInv37uXQoUMAs9gwnd6GJ06cAFhrw1xs2A7Op/nZMD8b5mfD/FzX5GfDdnA+zc+G6hrmN2jvA+ZKKe+XUs4DLwE7VndYqtPRo0eZmppi06ZNAAUbptPbcHJyEuAcNkzFhu3gfJqfDfOzYX42zM91TX42bAfn0/xsqK5hNmjXAad67p+uHuv3tYh4OyJejoj1tYxOtZifn2f9+ouS2DCZAQ3PY8NUbNgOzqf52TA/G+Znw/xc1+Rnw3ZwPs3Phuqq60PCfgxsKKXcDfwUeHHQSRGxJyJmImLmzJkzNf1o1cSG+dkwv6Eagh3HnNdifjbMz4b52TA/G+bn2rQdvBbzs+E1YJgN2nmgd3f+tuqxC0opH5VSFqq7zwFbBz1RKeXZUsp0KWX65ptvXs54tQzr1q3j1KneX4K2YTYDGk5iw1TqbFida8cGOJ/mZ8P8bJifDfNzbZqfa9N2cD7Nz4bqGmaD9k3g9ojYGBGTwE7gYO8JEXFrz93twDv1DVErde+99/Lee+/xwQcfAAQ2TKe34fnz5wHWYsNUbNgOzqf52TA/G+Znw/xc1+Rnw3ZwPs3PhuqauNIJpZRPI2If8CqwBni+lDIbEU8CM6WUg8C3I2I78CmdNxffvYpj1lWamJjgqaee4oEHHgC4E/hrG+bS23BxcRHgnA1zsWE7OJ/mZ8P8bJifDfNzXZOfDdvB+TQ/G6orSimN/ODp6ekyMzPTyM++lkXEsVLKdB3PZcPm1NXRhs3xWszPhvnZMD8b5mfDdnBtmp/XYn42zM+G+a2kYV0fEiZJkiRJkiRJukpu0EqSJEmSJElSQ9yglSRJkiRJkqSGuEErSZIkSZIkSQ1xg1aSJEmSJEmSGuIGrSRJkiRJkiQ1xA1aSZIkSZIkSWqIG7SSJEmSJEmS1BA3aCVJkiRJkiSpIW7QSpIkSZIkSVJD3KCVJEmSJEmSpIa4QStJkiRJkiRJDXGDVpIkSZIkSZIa4gatJEmSJEmSJDXEDVpJkiRJkiRJashQG7QRsS0i3o2IuYh4fMDx6yLiQHX8jYjYUPtItSKHDx9m8+bNAHfZMKduw6mpKYBb+o/bcPzZsB2cT/OzYX42zM+G+bmuyc+G7eB8mp8NBUNs0EbEGuBp4EFgC7ArIrb0nfYo8LtSyhTwd8Df1j1QLd/i4iJ79+7l0KFDALPYMJ3ehidOnABYa8NcbNgOzqf52TA/G+Znw/xc1+Rnw3ZwPs3Phuoa5jdo7wPmSinvl1LOAy8BO/rO2QG8WH39MvDViIj6hqmVOHr0KFNTU2zatAmgYMN0ehtOTk4CnMOGqdiwHZxP87NhfjbMz4b5ua7Jz4bt4Hyanw3VNcwG7TrgVM/909VjA88ppXwKfAzcWMcAtXLz8/OsX7++9yEbJjOg4XlsmIoN28H5ND8b5mfD/GyYn+ua/GzYDs6n+dlQXROj/GERsQfYU91diIjjo/z5NboJONv0IK7CF4DP7d+//0Ng80qeyIaN6W0IcOdyn6hFDSFXx9oaQqs6ZmoIzqeD2LDDhqNjw6Vs2GHD0XFtOlimjq5NB8vUEJxPB7Fhhw2bseyGw2zQzgO92/m3VY8NOud0REwANwAf9T9RKeVZ4FmAiJgppUwvZ9BNyzb2iPgS8L1SygMRMYMN0429t2F1/zTXeEPINf46G0J7OmYbu/PpUtnGbsOlso3dhktlG7sNl8o2dtemg2Uav2vTwbKN3fl0qWxjt+FS2ce+3O8d5i0O3gRuj4iNETEJ7AQO9p1zEPhG9fUjwM9KKWW5g1LtLjQEAhtm1H8drsWG2diwHZxP87NhfjbMz4b5ua7Jz4bt4Hyanw0FDLFBW72/xT7gVeAd4EellNmIeDIitlen7QdujIg54DvA46s1YF29voZ3YsN0BlyH52yYiw3bwfk0PxvmZ8P8bJif65r8bNgOzqf52VBd0dSme0TsqX79Oh3HXv9zjVrmsUN94/fPoTleix2Ovf7nGjXHXv9zjZpjr/+5Rs2x1/9co5Z57ODatCvz+L0WOxx7/c81ao69/ucatWt17I1t0EqSJEmSJEnSte6Kb3EQEc9HxG/jEp/+Fh3fj4i5iHg7Iu6pf5haCRu2gx3zs2F+NszPhvnZsB3smJ8N87NhfjbMz4bqGuZDwl4Atl3m+IPA7dVtD/D3vQcjYltEvFv9y7TkfTIi4rqIOFAdfyMiNgw9+lU2xNh3R8SZiHiruj3WxDgH6bvIX6CvYe9FDrwPbMWG49wQ+jr2T9R03rdm4LVow2bYsMOGFz2fHUfMhp+x4YXzbdgAXxM7bHjhXBs2wIYdNrzo+ew4Yjb8TIsa9h+/qGEMu6leSrniDdgAHL/EsX8AdvXcfxe4tfp6DfBrYBMwCfwS2NL3/d8Cnqm+3gkcGGZMq30bcuy7gaeaHuslxv9l4J5ut/6GwEPAISCAfwTmbDhet/6G/R37Gt4P/Ouga9GGNrRh8w2v4s/Cjja0oQ1b17DOjja0oQ1taENfEzN3tGE7G/Yd72/4xjDPO8xv0F7JOuBUz/3T1WMA99HZ+Hu/lHIeeAnY0ff9O4AXq69fBr4aEVHDuFZqmLGPrVLKz4FzlzllB/CD0vm35w+AyYi4tTpmwzFwNQ1LKb8Argf+ved4t6MNG2JDwIbOpw2z4QU27LBhQ3xNBGxow4bZELChr4kNs+EF11LDz/fst13SUB8SVv0K9CullLsGHHsF+JtSyuvV/deA75ZSZiLiEWBbKeWx6th+4E+Bf77++uu33nHHHVf82arHwsICc3NzfPLJJ2dLKTf3dqu+vgX4ZtXNhmOqtyPwBhdfe2fpdNtf3X8N+C6d/xpnwzFRU8M/B/4S+EMAO47Wcho6n44XG+Znw3ZYWFjg+PHji6WUiQF/p3Bdk4Br0/xcm+bna2J+NmyPY8eODWp4odnlvneihp8/D6zvuX9b9dggR4D/KKXsm56eLjMzlx2banTy5EkefvhhZmdnPxxweJ7Oi2yXDcfUFTouAH/Uc7/bcUPfeUewYWNqaggwW0rZB2DH0Vpmw0GO4LXYCBvmZ8N2OHnyJBs3bvz9JQ67rknAtWl+rk3z8zUxPxu2R0QMajiUOt7i4CDw9epNcO8HPi6l/KY6djWbtxqt3jYH6bzIztswlf42BfiTAdeiDceXDfMbtuGgc+04HmyYnw3bwdfE/GyYnw3z8zUxPxvmt6wuV/wN2oj4IfAV4KaIOA38FZ33LKWU8gzwEzpvgDtH530x/qLn298Ebo+IjdVgdgJ/duV/FtVp165dHDlyhLNnzwLcHRGPAr8HnoiIl4CPgH8DXseGY6u/I51eT0TEDcBbwL8A77D0WrThmLBhfitoCHYcCzbMz4bt0O0IXFf9HeNlfE1MxXVNfjbMz9fE/GzYSgeBfdV+2xe5eFP90srqf7rZQ8A/0fl0tieqx57cunVr0egBH3b+hwCerrr8CpguNkwDmLFhbnU1BLbbsRlX27B4LY4dG+Znw/xcm7aDa9P8XJvm52tifjbMbzkNu7ehPiRsNfh+GM2IiGOllOk6nsuGzamrow2b47WYnw3zs2F+NszPhu3g2jQ/r8X8bJifDfNbScM63oNWkiRJkiRJkrQMbtBKkiRJkiRJUkPcoJUkSZIkSZKkhrhBK0mSJEmSJEkNcYNWkiRJkiRJkhriBq0kSZIkSZIkNcQNWkmSJEmSJElqiBu0kiRJkiRJktQQN2glSZIkSZIkqSFu0EqSJEmSJElSQ9yglSRJkiRJkqSGuEErSZIkSZIkSQ1xg1aSJEmSJEmSGuIGrSRJkiRJkiQ1xA1aSZIkSZIkSWrIUBu0EbEtIt6NiLmIeHzA8d0RcSYi3qpuj9U/VK3E4cOH2bx5M8BdNsyp23Bqagrglv7jNhx/NmwH59P8bJifDfOzYX6ua/KzYTs4n+ZnQ8EQG7QRsQZ4GngQ2ALsiogtA049UEr54+r2XM3j1AosLi6yd+9eDh06BDCLDdPpbXjixAmAtTbMxYbt4Hyanw3zs2F+NszPdU1+NmwH59P8bKiuYX6D9j5grpTyfinlPPASsGN1h6U6HT16lKmpKTZt2gRQsGE6vQ0nJycBzmHDVGzYDs6n+dkwPxvmZ8P8XNfkZ8N2cD7Nz4bqGmaDdh1wquf+6eqxfl+LiLcj4uWIWD/oiSJiT0TMRMTMmTNnljFcLcf8/Dzr11+UxIbJDGh4HhumUmdDsGNTnE/zs2F+NszPhvm5Ns3PtWk7OJ/mZ0N11fUhYT8GNpRS7gZ+Crw46KRSyrOllOlSyvTNN99c049WTWyYnw3zG6oh2HHMeS3mZ8P8bJifDfOzYX6uTdvBazE/G14DhtmgnQd6d+dvqx67oJTyUSllobr7HLC1nuGpDuvWrePUqd5fgrZhNgMaTmLDVGzYDs6n+dkwPxvmZ8P8XNfkZ8N2cD7Nz4bqGmaD9k3g9ojYGBGTwE7gYO8JEXFrz93twDv1DVErde+99/Lee+/xwQcfAAQ2TKe34fnz5wHWYsNUbNgOzqf52TA/G+Znw/xc1+Rnw3ZwPs3PhuqauNIJpZRPI2If8CqwBni+lDIbEU8CM6WUg8C3I2I78CmdNxffvYpj1lWamJjgqaee4oEHHgC4E/hrG+bS23BxcRHgnA1zsWE7OJ/mZ8P8bJifDfNzXZOfDdvB+TQ/G6orSimN/ODp6ekyMzPTyM++lkXEsVLKdB3PZcPm1NXRhs3xWszPhvnZMD8b5mfDdnBtmp/XYn42zM+G+a2kYV0fEiZJkiRJkiRJukpu0EqSJEmSJElSQ9yglSRJkiRJkqSGuEErSZIkSZIkSQ1xg1aSJEmSJEmSGuIGrSRJkiRJkiQ1xA1aSZIkSZIkSWqIG7SSJEmSJEmS1BA3aCVJkiRJkiSpIW7QSpIkSZIkSVJD3KCVJEmSJEmSpIa4QStJkiRJkiRJDXGDVpIkSZIkSZIa4gatJEmSJEmSJDXEDVpJkiRJkiRJashQG7QRsS0i3o2IuYh4fMDx6yLiQHX8jYjYUPtItSKHDx9m8+bNAHfZMKduw6mpKYBb+o/bcPzZsB2cT/OzYX42zM+G+bmuyc+G7eB8mp8NBUNs0EbEGuBp4EFgC7ArIrb0nfYo8LtSyhTwd8Df1j1QLd/i4iJ79+7l0KFDALPYMJ3ehidOnABYa8NcbNgOzqf52TA/G+Znw/xc1+Rnw3ZwPs3Phuoa5jdo7wPmSinvl1LOAy8BO/rO2QG8WH39MvDViIj6hqmVOHr0KFNTU2zatAmgYMN0ehtOTk4CnMOGqdiwHZxP87NhfjbMz4b5ua7Jz4bt4Hyanw3VNcwG7TrgVM/909VjA88ppXwKfAzcWMcAtXLz8/OsX7++9yEbJjOg4XlsmIoN28H5ND8b5mfD/GyYn+ua/GzYDs6n+dlQXROj/GERsQfYU91diIjjo/z5NboJONv0IK7CF4DP7d+//0Ng80qeyIaN6W0IcOdyn6hFDSFXx9oaQqs6ZmoIzqeD2LDDhqNjw6Vs2GHD0XFtOlimjq5NB8vUEJxPB7Fhhw2bseyGw2zQzgO92/m3VY8NOud0REwANwAf9T9RKeVZ4FmAiJgppUwvZ9BNyzb2iPgS8L1SygMRMYMN0429t2F1/zTXeEPINf46G0J7OmYbu/PpUtnGbsOlso3dhktlG7sNl8o2dtemg2Uav2vTwbKN3fl0qWxjt+FS2ce+3O8d5i0O3gRuj4iNETEJ7AQO9p1zEPhG9fUjwM9KKWW5g1LtLjQEAhtm1H8drsWG2diwHZxP87NhfjbMz4b5ua7Jz4bt4Hyanw0FDLFBW72/xT7gVeAd4EellNmIeDIitlen7QdujIg54DvA46s1YF29voZ3YsN0BlyH52yYiw3bwfk0PxvmZ8P8bJif65r8bNgOzqf52VBd0dSme0TsqX79Oh3HXv9zjVrmsUN94/fPoTleix2Ovf7nGjXHXv9zjZpjr/+5Rs2x1/9co5Z57ODatCvz+L0WOxx7/c81ao69/ucatWt17I1t0EqSJEmSJEnSte6Kb3EQEc9HxG/jEp/+Fh3fj4i5iHg7Iu6pf5haCRu2gx3zs2F+NszPhvnZsB3smJ8N87NhfjbMz4bqGuZDwl4Atl3m+IPA7dVtD/D3vQcjYltEvFv9y7TkfTIi4rqIOFAdfyMiNgw9+lU2xNh3R8SZiHiruj3WxDgH6bvIX6CvYe9FDrwPbMWG49wQ+jr2T9R03rdm4LVow2bYsMOGFz2fHUfMhp+x4YXzbdgAXxM7bHjhXBs2wIYdNrzo+ew4Yjb8TIsa9h+/qGEMu6leSrniDdgAHL/EsX8AdvXcfxe4tfp6DfBrYBMwCfwS2NL3/d8Cnqm+3gkcGGZMq30bcuy7gaeaHuslxv9l4J5ut/6GwEPAISCAfwTmbDhet/6G/R37Gt4P/Ouga9GGNrRh8w2v4s/Cjja0oQ1b17DOjja0oQ1taENfEzN3tGE7G/Yd72/4xjDPO8xv0F7JOuBUz/3T1WMA99HZ+Hu/lHIeeAnY0ff9O4AXq69fBr4aEVHDuFZqmLGPrVLKz4FzlzllB/CD0vm35w+AyYi4tTpmwzFwNQ1LKb8Argf+ved4t6MNG2JDwIbOpw2z4QU27LBhQ3xNBGxow4bZELChr4kNs+EF11LDz/fst13SUB8SVv0K9CullLsGHHsF+JtSyuvV/deA75ZSZiLiEWBbKeWx6th+4E+Bf77++uu33nHHHVf82arHwsICc3NzfPLJJ2dLKTf3dqu+vgX4ZtXNhmOqtyPwBhdfe2fpdNtf3X8N+C6d/xpnwzFRU8M/B/4S+EMAO47Wcho6n44XG+Znw3ZYWFjg+PHji6WUiQF/p3Bdk4Br0/xcm+bna2J+NmyPY8eODWp4odnlvneihp8/D6zvuX9b9dggR4D/KKXsm56eLjMzlx2banTy5EkefvhhZmdnPxxweJ7Oi2yXDcfUFTouAH/Uc7/bcUPfeUewYWNqaggwW0rZB2DH0Vpmw0GO4LXYCBvmZ8N2OHnyJBs3bvz9JQ67rknAtWl+rk3z8zUxPxu2R0QMajiUOt7i4CDw9epNcO8HPi6l/KY6djWbtxqt3jYH6bzIztswlf42BfiTAdeiDceXDfMbtuGgc+04HmyYnw3bwdfE/GyYnw3z8zUxPxvmt6wuV/wN2oj4IfAV4KaIOA38FZ33LKWU8gzwEzpvgDtH530x/qLn298Ebo+IjdVgdgJ/duV/FtVp165dHDlyhLNnzwLcHRGPAr8HnoiIl4CPgH8DXseGY6u/I51eT0TEDcBbwL8A77D0WrThmLBhfitoCHYcCzbMz4bt0O0IXFf9HeNlfE1MxXVNfjbMz9fE/GzYSgeBfdV+2xe5eFP90srqf7rZQ8A/0fl0tieqx57cunVr0egBH3b+hwCerrr8CpguNkwDmLFhbnU1BLbbsRlX27B4LY4dG+Znw/xcm7aDa9P8XJvm52tifjbMbzkNu7ehPiRsNfh+GM2IiGOllOk6nsuGzamrow2b47WYnw3zs2F+NszPhu3g2jQ/r8X8bJifDfNbScM63oNWkiRJkiRJkrQMbtBKkiRJkiRJUkPcoJUkSZIkSZKkhrhBK0mSJEmSJEkNcYNWkiRJkiRJkhriBq0kSZIkSZIkNcQNWkmSJEmSJElqiBu0kiRJkiRJktQQN2glSZIkSZIkqSFu0EqSJEmSJElSQ9yglSRJkiRJkqSGuEErSZIkSZIkSQ1xg1aSJEmSJEmSGuIGrSRJkiRJkiQ1ZKgN2ojYFhHvRsRcRDw+4PjuiDgTEW9Vt8fqH6pW4vDhw2zevBngLhvm1G04NTUFcEv/cRuOPxu2g/NpfjbMz4b52TA/1zX52bAdnE/zs6FgiA3aiFgDPA08CGwBdkXElgGnHiil/HF1e67mcWoFFhcX2bt3L4cOHQKYxYbp9DY8ceIEwFob5mLDdnA+zc+G+dkwPxvm57omPxu2g/NpfjZU1zC/QXsfMFdKeb+Uch54CdixusNSnY4ePcrU1BSbNm0CKNgwnd6Gk5OTAOewYSo2bAfn0/xsmJ8N87Nhfq5r8rNhOzif5mdDdQ2zQbsOONVz/3T1WL+vRcTbEfFyRKyvZXSqxfz8POvXX5TEhskMaHgeG6Ziw3ZwPs3PhvnZMD8b5ue6Jj8btoPzaX42VFddHxL2Y2BDKeVu4KfAi4NOiog9ETETETNnzpyp6UerJjbMz4b5DdUQ7DjmvBbzs2F+NszPhvnZMD/Xpu3gtZifDa8Bw2zQzgO9u/O3VY9dUEr5qJSyUN19Dtg66IlKKc+WUqZLKdM333zzcsarZVi3bh2nTvX+ErQNsxnQcBIbplJnw+pcOzbA+TQ/G+Znw/xsmJ9r0/xcm7aD82l+NlTXMBu0bwK3R8TGiJgEdgIHe0+IiFt77m4H3qlviFqpe++9l/fee48PPvgAILBhOr0Nz58/D7AWG6Ziw3ZwPs3PhvnZMD8b5ue6Jj8btoPzaX42VNfElU4opXwaEfuAV4E1wPOllNmIeBKYKaUcBL4dEduBT+m8ufjuVRyzrtLExARPPfUUDzzwAMCdwF/bMJfehouLiwDnbJiLDdvB+TQ/G+Znw/xsmJ/rmvxs2A7Op/nZUF1RSmnkB09PT5eZmZlGfva1LCKOlVKm63guGzanro42bI7XYn42zM+G+dkwPxu2g2vT/LwW87NhfjbMbyUN6/qQMEmSJEmSJEnSVXKDVpIkSZIkSZIa4gatJEmSJEmSJDXEDVpJkiRJkiRJaogbtJIkSZIkSZLUEDdoJUmSJEmSJKkhbtBKkiRJkiRJUkPcoJUkSZIkSZKkhrhBK0mSJEmSJEkNcYNWkiRJkiRJkhriBq0kSZIkSZIkNcQNWkmSJEmSJElqiBu0kiRJkiRJktQQN2glSZIkSZIkqSFu0EqSJEmSJElSQ4baoI2IbRHxbkTMRcTjA45fFxEHquNvRMSG2keqFTl8+DCbN28GuMuGOXUbTk1NAdzSf9yG48+G7eB8mp8N87NhfjbMz3VNfjZsB+fT/GwoGGKDNiLWAE8DDwJbgF0RsaXvtEeB35VSpoC/A/627oFq+RYXF9m7dy+HDh0CmMWG6fQ2PHHiBMBaG+Ziw3ZwPs3PhvnZMD8b5ue6Jj8btoPzaX42VNcwv0F7HzBXSnm/lHIeeAnY0XfODuDF6uuXga9GRNQ3TK3E0aNHmZqaYtOmTQAFG6bT23BychLgHDZMxYbt4Hyanw3zs2F+NszPdU1+NmwH59P8bKiuYTZo1wGneu6frh4beE4p5VPgY+DGOgaolZufn2f9+vW9D9kwmQENz2PDVGzYDs6n+dkwPxvmZ8P8XNfkZ8N2cD7Nz4bqmhjlD4uIPcCe6u5CRBwf5c+v0U3A2aYHcRW+AHxu//79HwKbV/JENmxMb0OAO5f7RC1qCLk61tYQWtUxU0NwPh3Ehh02HB0bLmXDDhuOjmvTwTJ1dG06WKaG4Hw6iA07bNiMZTccZoN2Hujdzr+temzQOacjYgK4Afio/4lKKc8CzwJExEwpZXo5g25atrFHxJeA75VSHoiIGWyYbuy9Dav7p7nGG0Ku8dfZENrTMdvYnU+XyjZ2Gy6Vbew2XCrb2G24VLaxuzYdLNP4XZsOlm3szqdLZRu7DZfKPvblfu8wb3HwJnB7RGyMiElgJ3Cw75yDwDeqrx8BflZKKcsdlGp3oSEQ2DCj/utwLTbMxobt4Hyanw3zs2F+NszPdU1+NmwH59P8bChgiA3a6v0t9gGvAu8APyqlzEbEkxGxvTptP3BjRMwB3wEeX60B6+r1NbwTG6Yz4Do8Z8NcbNgOzqf52TA/G+Znw/xc1+Rnw3ZwPs3PhuqKpjbdI2JP9evX6Tj2+p9r1DKPHeobv38OzfFa7HDs9T/XqDn2+p9r1Bx7/c81ao69/ucatcxjB9emXZnH77XY4djrf65Rc+z1P9eoXatjb2yDVpIkSZIkSZKudVd8i4OIeD4ifhuX+PS36Ph+RMxFxNsRcU/9w9RK2LAd7JifDfOzYX42zM+G7WDH/GyYnw3zs2F+NlTXMB8S9gKw7TLHHwRur257gL/vPRgR2yLi3epfpiXvkxER10XEger4GxGxYejRr7Ihxr47Is5ExFvV7bEmxjlI30X+An0Ney9y4H1gKzYc54bQ17F/oqbzvjUDr0UbNsOGHTa86PnsOGI2/IwNL5xvwwb4mthhwwvn2rABNuyw4UXPZ8cRs+FnWtSw//hFDWPYTfVSyhVvwAbg+CWO/QOwq+f+u8Ct1ddrgF8Dm4BJ4JfAlr7v/xbwTPX1TuDAMGNa7duQY98NPNX0WC8x/i8D93S79TcEHgIOAQH8IzBnw/G69Tfs79jX8H7gXwddiza0oQ2bb3gVfxZ2tKENbdi6hnV2tKENbWhDG/qamLmjDdvZsO94f8M3hnneYX6D9krWAad67p+uHgO4j87G3/ullPPAS8COvu/fAbxYff0y8NWIiBrGtVLDjH1slVJ+Dpy7zCk7gB+Uzr89fwBMRsSt1TEbjoGraVhK+QVwPfDvPce7HW3YEBsCNnQ+bZgNL7Bhhw0b4msiYEMbNsyGgA19TWyYDS+4lhp+vme/7ZKG+pCw6legXyml3DXg2CvA35RSXq/uvwZ8t5QyExGPANtKKY9Vx/YDfwr88/XXX7/1jjvuuOLPVj0WFhaYm5vjk08+OVtKubm3W/X1LcA3q242HFO9HYE3uPjaO0un2/7q/mvAd+n81zgbjomaGv458JfAHwLYcbSW09D5dLzYMD8btsPCwgLHjx9fLKVMDPg7heuaBFyb5ufaND9fE/OzYXscO3ZsUMMLzS73vRM1/Px5YH3P/duqxwY5AvxHKWXf9PR0mZm57NhUo5MnT/Lwww8zOzv74YDD83ReZLtsOKau0HEB+KOe+92OG/rOO4ING1NTQ4DZUso+ADuO1jIbDnIEr8VG2DA/G7bDyZMn2bhx4+8vcdh1TQKuTfNzbZqfr4n52bA9ImJQw6HU8RYHB4GvV2+Cez/wcSnlN9Wxq9m81Wj1tjlI50V23oap9LcpwJ8MuBZtOL5smN+wDQeda8fxYMP8bNgOvibmZ8P8bJifr4n52TC/ZXW54m/QRsQPga8AN0XEaeCv6LxnKaWUZ4Cf0HkD3Dk674vxFz3f/iZwe0RsrAazE/izK/+zqE67du3iyJEjnD17FuDuiHgU+D3wRES8BHwE/BvwOjYcW/0d6fR6IiJuAN4C/gV4h6XXog3HhA3zW0FDsONYsGF+NmyHbkfguurvGC/ja2Iqrmvys2F+vibmZ8NWOgjsq/bbvsjFm+qXVlb/080eAv6JzqezPVE99uTWrVuLRg/4sPM/BPB01eVXwHSxYRrAjA1zq6shsN2OzbjahsVrcezYMD8b5ufatB1cm+bn2jQ/XxPzs2F+y2nYvQ31IWGrwffDaEZEHCulTNfxXDZsTl0dbdgcr8X8bJifDfOzYX42bAfXpvl5LeZnw/xsmN9KGtbxHrSSJEmSJEmSpGVwg1aSJEmSJEmSGuIGrSRJkiRJkiQ1xA1aSZIkSZIkSWqIG7SSJEmSJEmS1BA3aCVJkiRJkiSpIW7QSpIkSZIkSVJD3KCVJEmSJEmSpIa4QStJkiRJkiRJDXGDVpIkSZIkSZIa4gatJEmSJEmSJDXEDVpJkiRJkiRJaogbtJIkSZIkSZLUEDdoJUmSJEmSJKkhbtBKkiRJkiRJUkOG2qCNiG0R8W5EzEXE4wOO746IMxHxVnV7rP6haiUOHz7M5s2bAe6yYU7dhlNTUwC39B+34fizYTs4n+Znw/xsmJ8N83Ndk58N28H5ND8bCobYoI2INcDTwIPAFmBXRGwZcOqBUsofV7fnah6nVmBxcZG9e/dy6NAhgFlsmE5vwxMnTgCstWEuNmwH59P8bJifDfOzYX6ua/KzYTs4n+ZnQ3UN8xu09wFzpZT3SynngZeAHas7LNXp6NGjTE1NsWnTJoCCDdPpbTg5OQlwDhumYsN2cD7Nz4b52TA/G+bnuiY/G7aD82l+NlTXMBu064BTPfdPV4/1+1pEvB0RL0fE+kFPFBF7ImImImbOnDmzjOFqOebn51m//qIkNkxmQMPz2DCVOhuCHZvifJqfDfOzYX42zM+1aX6uTdvB+TQ/G6qrrg8J+zGwoZRyN/BT4MVBJ5VSni2lTJdSpm+++eaafrRqYsP8bJjfUA3BjmPOazE/G+Znw/xsmJ8N83Nt2g5ei/nZ8BowzAbtPNC7O39b9dgFpZSPSikL1d3ngK31DE91WLduHadO9f4StA2zGdBwEhumYsN2cD7Nz4b52TA/G+bnuiY/G7aD82l+NlTXMBu0bwK3R8TGiJgEdgIHe0+IiFt77m4H3qlviFqpe++9l/fee48PPvgAILBhOr0Nz58/D7AWG6Ziw3ZwPs3PhvnZMD8b5ue6Jj8btoPzaX42VNfElU4opXwaEfuAV4E1wPOllNmIeBKYKaUcBL4dEduBT+m8ufjuVRyzrtLExARPPfUUDzzwAMCdwF/bMJfehouLiwDnbJiLDdvB+TQ/G+Znw/xsmJ/rmvxs2A7Op/nZUF1RSmnkB09PT5eZmZlGfva1LCKOlVKm63guGzanro42bI7XYn42zM+G+dkwPxu2g2vT/LwW87NhfjbMbyUN6/qQMEmSJEmSJEnSVXKDVpIkSZIkSZIa4gatJEmSJEmSJDXEDVpJkiRJkiRJaogbtJIkSZIkSZLUEDdoJUmSJEmSJKkhbtBKkiRJkiRJUkPcoJUkSZIkSZKkhrhBK0mSJEmSJEkNcYNWkiRJkiRJkhriBq0kSZIkSZIkNcQNWkmSJEmSJElqiBu0kiRJkiRJktQQN2glSZIkSZIkqSFu0EqSJEmSJElSQ4baoI2IbRHxbkTMRcTjA45fFxEHquNvRMSG2keqFTl8+DCbN28GuMuGOXUbTk1NAdzSf9yG48+G7eB8mp8N87NhfjbMz3VNfjZsB+fT/GwoGGKDNiLWAE8DDwJbgF0RsaXvtEeB35VSpoC/A/627oFq+RYXF9m7dy+HDh0CmMWG6fQ2PHHiBMBaG+Ziw3ZwPs3PhvnZMD8b5ue6Jj8btoPzaX42VNcwv0F7HzBXSnm/lHIeeAnY0XfODuDF6uuXga9GRNQ3TK3E0aNHmZqaYtOmTQAFG6bT23BychLgHDZMxYbt4Hyanw3zs2F+NszPdU1+NmwH59P8bKiuYTZo1wGneu6frh4beE4p5VPgY+DGOgaolZufn2f9+vW9D9kwmQENz2PDVGzYDs6n+dkwPxvmZ8P8XNfkZ8N2cD7Nz4bqmhjlD4uIPcCe6u5CRBwf5c+v0U3A2aYHcRW+AHxu//79HwKbV/JENmxMb0OAO5f7RC1qCLk61tYQWtUxU0NwPh3Ehh02HB0bLmXDDhuOjmvTwTJ1dG06WKaG4Hw6iA07bNiMZTccZoN2Hujdzr+temzQOacjYgK4Afio/4lKKc8CzwJExEwpZXo5g25atrFHxJeA75VSHoiIGWyYbuy9Dav7p7nGG0Ku8dfZENrTMdvYnU+XyjZ2Gy6Vbew2XCrb2G24VLaxuzYdLNP4XZsOlm3szqdLZRu7DZfKPvblfu8wb3HwJnB7RGyMiElgJ3Cw75yDwDeqrx8BflZKKcsdlGp3oSEQ2DCj/utwLTbMxobt4Hyanw3zs2F+NszPdU1+NmwH59P8bChgiA3a6v0t9gGvAu8APyqlzEbEkxGxvTptP3BjRMwB3wEeX60B6+r1NbwTG6Yz4Do8Z8NcbNgOzqf52TA/G+Znw/xc1+Rnw3ZwPs3PhuqKpjbdI2JP9evX6Tj2+p9r1DKPHeobv38OzfFa7HDs9T/XqDn2+p9r1Bx7/c81ao69/ucatcxjB9emXZnH77XY4djrf65Rc+z1P9eoXatjb2yDVpIkSZIkSZKudcO8B60kSZIkSZIkaRWs+gZtRGyLiHcjYi4ilrxPRkRcFxEHquNvRMSG1R7TsIYY++6IOBMRb1W3x5oY5yAR8XxE/DYijl/ieETE96t/trcj4p7LPJcNG2DDDhteONeGDaizYXW+HUfMhp+x4YXzbdgAXxM7bHjhXBs2wIYdNrzofDuOmA0/Y8M+pZRVuwFrgF8Dm4BJ4JfAlr5zvgU8U329EziwmmOqeey7gaeaHuslxv9l4B7g+CWOPwQcovMpgfcDb9hwvG42tKENm7/V1dCONmz6ZkMbNn2rq6MNbWhDG9rQ18TMHW1ow0vdVvs3aO8D5kop75dSzgMvATv6ztkBvFh9/TLw1YiIVR7XMIYZ+9gqpfwcOHeZU3YAPygdvwA+HxG3DjjPhg2xIWDDLhs2pMaGYMdG2PACG3bYsCG+JgI27LJhQ2wI2LCXHRtgwwts2Ge1N2jXAad67p+uHht4TinlU+Bj4MZVHtcwhhk7wNeqX1l+OSLWj2ZotRj2n8+G48uGn7GhDZsy7D/fsOfacfRseDEb2rApviZ+xoY2bIoNP9P2hsOea8fRs+HF2t7wAj8kbGV+DGwopdwN/JTP/quE8rBhfjbMz4btYMf8bJifDfOzYX42zM+G7WDH/K6phqu9QTsP9O5w31Y9NvCciJgAbgA+WuVxDeOKYy+lfFRKWajuPgdsHdHY6jBMm2HPs2EzbIgNB51jw5EatuGw59px9GxYseHSc2w4Ur4mYsNB59hwpGzINdNw2HPtOHo2rFwjDS9Y7Q3aN4HbI2JjREzSeUPig33nHAS+UX39CPCzUjrvqtuwK4697z0ktgPvjHB8K3UQ+Hr16XL3Ax+XUn4z4Dwbji8bYsO+57Lh6A3bEOw4rmxYseFFz2fD0fM1ERv2PZcNR8+GXDMNwY7jyoaVa6ThZ8rqf7rZQ8A/0fl0tieqx54Etldf/w/A/w3MAUeBTas9phrH/n8As3Q+be6/AXc0Peaesf8Q+A3wezrvd/Eo8E3gm9XxAJ6u/tl+BUzb0IY2tKENV6+hHW3Y9M2GNmxLRxva0IY2tKGviVk72tCGl7pF9c2SJEmSJEmSpBHzQ8IkSZIkSZIkqSFu0EqSJEmSJElSQ9yglSRJkiRJkqSGuEErSZIkSZIkSQ1xg1aSJEmSJEmSGuIGrSRJkiRJkiQ1xA1aSZIkSZIkSWqIG7SSJEmSJEmS1JD/H5QHJfH7geDuAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# plot some example audio \n", + "nrows = 20\n", + "ncols = 12\n", + "zoom = 2\n", + "fig, axs = plt.subplots(ncols=ncols, nrows = nrows,figsize = (ncols*zoom, nrows+zoom/1.5))\n", + "for i, turn in tqdm(enumerate(df_audio['audio'].values), total = nrows*ncols):\n", + " ax = axs.flatten()[i]\n", + " ax.plot(turn)\n", + " if i == nrows*ncols -1:\n", + " break" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Save as `csv`" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'df_audio' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "\u001b[1;32m/Users/u517177/3.1_Make_audio_and_spec_cols.ipynb Cell 16\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0m df_audio\u001b[39m.\u001b[39mto_csv(\u001b[39m\"\u001b[39m\u001b[39mdf_audio.csv\u001b[39m\u001b[39m\"\u001b[39m, index\u001b[39m=\u001b[39m\u001b[39mFalse\u001b[39;00m)\n\u001b[1;32m 2\u001b[0m df_audio\n", + "\u001b[0;31mNameError\u001b[0m: name 'df_audio' is not defined" + ] + } + ], + "source": [ + "df_audio.to_csv(\"df_audio.csv\", index=False)\n", + "df_audio" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10.0" + }, + "vscode": { + "interpreter": { + "hash": "46fbe6e04536e1e35bdd7bb388ca2958a28e6ed66e8c3e8ff3ca43c0ea6b4829" + } + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/notebooks/Make_audio_column_Barbara.ipynb b/notebooks/Make_audio_column_Barbara.ipynb new file mode 100644 index 0000000..eb033c7 --- /dev/null +++ b/notebooks/Make_audio_column_Barbara.ipynb @@ -0,0 +1,195 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 66, + "metadata": {}, + "outputs": [], + "source": [ + "# to ensure that the modules can be imported, as they are located in a different folder, add the package root to the path:\n", + "\n", + "import sys\n", + "sys.path.insert(0, \"../\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": {}, + "outputs": [], + "source": [ + "import librosa\n", + "from sktalk import read_audio\n", + "import IPython.display as ipd" + ] + }, + { + "cell_type": "code", + "execution_count": 86, + "metadata": {}, + "outputs": [], + "source": [ + "import subprocess\n", + "import json\n", + "import numpy as np\n", + "\n", + "\n", + "def load_audio(file_path):\n", + " cmd = [\n", + " \"ffprobe\",\n", + " \"-v\", \"quiet\",\n", + " \"-print_format\", \"json\",\n", + " \"-show_streams\",\n", + " file_path\n", + " ]\n", + "\n", + " result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)\n", + " output = json.loads(result.stdout)\n", + "\n", + " sample_rate = None\n", + " for stream in output[\"streams\"]:\n", + " if stream[\"codec_type\"] == \"audio\":\n", + " sample_rate = stream[\"sample_rate\"]\n", + " no_channels = str(stream[\"channels\"])\n", + "\n", + " if sample_rate is None:\n", + " raise ValueError(\"No audio stream found in the file\")\n", + "\n", + " cmd = [\"ffmpeg\", \"-i\", file_path, '-f', 's16le',\n", + " '-acodec', 'pcm_s16le',\n", + " '-ar', sample_rate,\n", + " '-ac', no_channels,\n", + " '-']\n", + "\n", + " print((' ').join(cmd))\n", + " pipe = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n", + " raw_audio = pipe.stdout\n", + " audio_array = np.frombuffer(raw_audio, dtype=\"int16\")\n", + " audio_array = audio_array.astype(np.float32) / np.iinfo(np.int16).max\n", + "\n", + " return audio_array, int(sample_rate)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 87, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ffmpeg -i ../data/catalan_demo.wav -f s16le -acodec pcm_s16le -ar 16000 -ac 2 -\n", + "22050 16000\n" + ] + } + ], + "source": [ + "wavaddress = \"../data/catalan_demo.wav\"\n", + "\n", + "d_ffmpeg, r_ffmpeg = load_audio(wavaddress)\n", + "d_librosa, r_librosa = librosa.load(wavaddress)\n", + "\n", + "print(r_librosa, r_ffmpeg)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ipd.Audio(wavaddress) # load a local WAV file" + ] + }, + { + "cell_type": "code", + "execution_count": 88, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 88, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ipd.Audio(d_ffmpeg, rate=r_ffmpeg)" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [], + "source": [ + "import soundfile as sf\n", + "\n", + "sf.write('../data/output.wav', d_librosa, r_librosa, subtype='PCM_24')" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + }, + "vscode": { + "interpreter": { + "hash": "46fbe6e04536e1e35bdd7bb388ca2958a28e6ed66e8c3e8ff3ca43c0ea6b4829" + } + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/notebooks/exploration.ipynb b/notebooks/exploration.ipynb index 5949ffa..ab344fb 100644 --- a/notebooks/exploration.ipynb +++ b/notebooks/exploration.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 27, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -18,65 +18,310 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## The `my_module` module\n", + "## The `from_convokit` module\n", "\n", - "Import and use the `hello()` method:" + "Here, we import a single class from the `csv_to_json` module, and use it." ] }, { "cell_type": "code", - "execution_count": 47, + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import sktalk.from_convokit as ck" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "df = pd.read_csv(\"../data/ulwa_testdata_convokit_format.csv\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ID column is not present in utterances dataframe, generated ID column from dataframe index...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "10it [00:00, 4043.87it/s]\n" + ] + } + ], + "source": [ + "corpus = ck.Corpus.from_pandas(utterances_df = df)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "corpus.dump(name ='testcorpus', base_path=\"../data/\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The `csv_to_json` module\n", + "\n", + "Here, we import a single class from the `csv_to_json` module, and use it." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "import sktalk.csv_to_json as cj\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, "metadata": {}, "outputs": [ { "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
beginendparticipantutterancetranslationsourceutterance_raw
000:00:00.91700:00:05.604TangU oughs inim tï samting yanLorem ipsum dolor sit amet./ulwa1/ulwa014U oughs inim tï samting yangama ul matï akïnakape
100:00:04.83000:00:09.080Yanmbam ndul ma wandam anaAt neque fugit eum reprehenderit labore et exe.../ulwa1/ulwa014wimbam ndul ma wandam anapa ol welunda nïkap t...
200:00:06.09000:00:09.450TangMï inim wandam bai anapa nda veritatis tempore sit vitae quaerat sed cons.../ulwa1/ulwa014Mï inim wandam bai anapa ndïtï ka welunda unan
300:00:09.53400:00:10.333Yanlunda we ndïmïne inNaN/ulwa1/ulwa014ata welunda we ndïmïne ind
400:00:10.33300:00:11.143Tangkïnakape akïnakaNaN/ulwa1/ulwa014i akïnakape akïnakap
500:00:11.14300:00:18.240Yancoughs ndïmïne we ndul wa le we ndïtï akïnakap...Et illo facere vel magni necessitatibus est as.../ulwa1/ulwa014[coughs] I inim oughs ka lopop mananda bai kïk...
600:00:11.47700:00:12.205TangmanandaNaN/ulwa1/ulwa014n mananda ndïtï ka akïnakape wimbam
700:00:14.39000:00:15.696YandaNaN/ulwa1/ulwa014da ndïtï ka
800:00:17.97200:00:20.722Tange kïkal awi akïnakapeonsequatur amet qui nisi facilis et perferendi.../ulwa1/ulwa014e kïkal awi akïnakape manï lï
900:00:18.24000:00:21.970Yanatïm inim.itae quaerat sed consequatur amet/ulwa1/ulwa014atïm inim.
\n", + "
" + ], "text/plain": [ - "'Hello Person!'" + " begin end participant \\\n", + "0 00:00:00.917 00:00:05.604 Tang \n", + "1 00:00:04.830 00:00:09.080 Yan \n", + "2 00:00:06.090 00:00:09.450 Tang \n", + "3 00:00:09.534 00:00:10.333 Yan \n", + "4 00:00:10.333 00:00:11.143 Tang \n", + "5 00:00:11.143 00:00:18.240 Yan \n", + "6 00:00:11.477 00:00:12.205 Tang \n", + "7 00:00:14.390 00:00:15.696 Yan \n", + "8 00:00:17.972 00:00:20.722 Tang \n", + "9 00:00:18.240 00:00:21.970 Yan \n", + "\n", + " utterance \\\n", + "0 U oughs inim tï samting yan \n", + "1 mbam ndul ma wandam ana \n", + "2 Mï inim wandam bai anapa nd \n", + "3 lunda we ndïmïne in \n", + "4 kïnakape akïnaka \n", + "5 coughs ndïmïne we ndul wa le we ndïtï akïnakap... \n", + "6 mananda \n", + "7 da \n", + "8 e kïkal awi akïnakape \n", + "9 atïm inim. \n", + "\n", + " translation source \\\n", + "0 Lorem ipsum dolor sit amet. /ulwa1/ulwa014 \n", + "1 At neque fugit eum reprehenderit labore et exe... /ulwa1/ulwa014 \n", + "2 a veritatis tempore sit vitae quaerat sed cons... /ulwa1/ulwa014 \n", + "3 NaN /ulwa1/ulwa014 \n", + "4 NaN /ulwa1/ulwa014 \n", + "5 Et illo facere vel magni necessitatibus est as... /ulwa1/ulwa014 \n", + "6 NaN /ulwa1/ulwa014 \n", + "7 NaN /ulwa1/ulwa014 \n", + "8 onsequatur amet qui nisi facilis et perferendi... /ulwa1/ulwa014 \n", + "9 itae quaerat sed consequatur amet /ulwa1/ulwa014 \n", + "\n", + " utterance_raw \n", + "0 U oughs inim tï samting yangama ul matï akïnakape \n", + "1 wimbam ndul ma wandam anapa ol welunda nïkap t... \n", + "2 Mï inim wandam bai anapa ndïtï ka welunda unan \n", + "3 ata welunda we ndïmïne ind \n", + "4 i akïnakape akïnakap \n", + "5 [coughs] I inim oughs ka lopop mananda bai kïk... \n", + "6 n mananda ndïtï ka akïnakape wimbam \n", + "7 da ndïtï ka \n", + "8 e kïkal awi akïnakape manï lï \n", + "9 atïm inim. " ] }, - "execution_count": 47, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "import sktalk.my_module as mod\n", - "\n", - "mod.hello(\"Person\")\n" + "corpus = cj.Corpus(\"../data/ulwa_testdata_sktalk_format.csv\")\n", + "corpus.return_dataframe()\n", + "corpus.df" ] }, { - "attachments": {}, - "cell_type": "markdown", + "cell_type": "code", + "execution_count": 8, "metadata": {}, + "outputs": [], "source": [ - "## The `Demo` module\n", - "\n", - "Here, we import a single class from the `demo` module, and use it." + "corpus.return_json()" ] }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'Person'" + "'{\"begin\":{\"0\":\"00:00:00.917\",\"1\":\"00:00:04.830\",\"2\":\"00:00:06.090\",\"3\":\"00:00:09.534\",\"4\":\"00:00:10.333\",\"5\":\"00:00:11.143\",\"6\":\"00:00:11.477\",\"7\":\"00:00:14.390\",\"8\":\"00:00:17.972\",\"9\":\"00:00:18.240\"},\"end\":{\"0\":\"00:00:05.604\",\"1\":\"00:00:09.080\",\"2\":\"00:00:09.450\",\"3\":\"00:00:10.333\",\"4\":\"00:00:11.143\",\"5\":\"00:00:18.240\",\"6\":\"00:00:12.205\",\"7\":\"00:00:15.696\",\"8\":\"00:00:20.722\",\"9\":\"00:00:21.970\"},\"participant\":{\"0\":\"Tang\",\"1\":\"Yan\",\"2\":\"Tang\",\"3\":\"Yan\",\"4\":\"Tang\",\"5\":\"Yan\",\"6\":\"Tang\",\"7\":\"Yan\",\"8\":\"Tang\",\"9\":\"Yan\"},\"utterance\":{\"0\":\"U oughs inim t\\\\u00ef samting yan\",\"1\":\"mbam ndul ma wandam ana\",\"2\":\"M\\\\u00ef inim wandam bai anapa nd\",\"3\":\"lunda we nd\\\\u00efm\\\\u00efne in\",\"4\":\"k\\\\u00efnakape ak\\\\u00efnaka\",\"5\":\"coughs nd\\\\u00efm\\\\u00efne we ndul wa le we nd\\\\u00eft\\\\u00ef ak\\\\u00efnakape malimap mat\\\\u00ef yawa mananda\",\"6\":\"mananda\",\"7\":\"da\",\"8\":\"e k\\\\u00efkal awi ak\\\\u00efnakape\",\"9\":\"at\\\\u00efm inim.\"},\"translation\":{\"0\":\"Lorem ipsum dolor sit amet.\",\"1\":\"At neque fugit eum reprehenderit labore et exercitationem voluptatem. eos odio aspernatur.\",\"2\":\"a veritatis tempore sit vitae quaerat sed consequatur amet qui nisi facilis et perferendis nisi ut maiores consequatur.\",\"3\":null,\"4\":null,\"5\":\"Et illo facere vel magni necessitatibus est aspernatur numquam\",\"6\":null,\"7\":null,\"8\":\"onsequatur amet qui nisi facilis et perferendis nisi ut\",\"9\":\"itae quaerat sed consequatur amet\"},\"source\":{\"0\":\"\\\\/ulwa1\\\\/ulwa014\",\"1\":\"\\\\/ulwa1\\\\/ulwa014\",\"2\":\"\\\\/ulwa1\\\\/ulwa014\",\"3\":\"\\\\/ulwa1\\\\/ulwa014\",\"4\":\"\\\\/ulwa1\\\\/ulwa014\",\"5\":\"\\\\/ulwa1\\\\/ulwa014\",\"6\":\"\\\\/ulwa1\\\\/ulwa014\",\"7\":\"\\\\/ulwa1\\\\/ulwa014\",\"8\":\"\\\\/ulwa1\\\\/ulwa014\",\"9\":\"\\\\/ulwa1\\\\/ulwa014\"},\"utterance_raw\":{\"0\":\"U oughs inim t\\\\u00ef samting yangama ul mat\\\\u00ef ak\\\\u00efnakape\",\"1\":\"wimbam ndul ma wandam anapa ol welunda n\\\\u00efkap tu mananda yangama \",\"2\":\"M\\\\u00ef inim wandam bai anapa nd\\\\u00eft\\\\u00ef ka welunda unan\",\"3\":\"ata welunda we nd\\\\u00efm\\\\u00efne ind\",\"4\":\"i ak\\\\u00efnakape ak\\\\u00efnakap\",\"5\":\"[coughs] I inim oughs ka lopop mananda bai k\\\\u00efkal yangama we ini\",\"6\":\"n mananda nd\\\\u00eft\\\\u00ef ka ak\\\\u00efnakape wimbam\",\"7\":\"da nd\\\\u00eft\\\\u00ef ka\",\"8\":\"e k\\\\u00efkal awi ak\\\\u00efnakape man\\\\u00ef l\\\\u00ef\",\"9\":\"at\\\\u00efm inim.\"}}'" ] }, - "execution_count": 48, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "from sktalk.demo import Demo\n", - "\n", - "person = Demo(\"Person\")\n", - "\n", - "person.name" + "corpus.json" ] } ], @@ -96,7 +341,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.6" + "version": "3.9.10" }, "orig_nbformat": 4, "vscode": { diff --git a/setup.cfg b/setup.cfg index 4c5a3a3..2af455f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,7 +35,8 @@ zip_safe = False python_requires = >=3.7 include_package_data = True packages = find: -install_requires = +install_requires = + pandas [options.data_files] # This section requires setuptools>=40.6.0 diff --git a/sktalk/csv_to_json.py b/sktalk/csv_to_json.py new file mode 100644 index 0000000..7abaa56 --- /dev/null +++ b/sktalk/csv_to_json.py @@ -0,0 +1,14 @@ +"""Documentation about the scikit-talk module - csv to json.""" +import pandas as pd + + +class Corpus: + def __init__(self, path): + self.path = path + + def return_dataframe(self): + self.df = pd.read_csv(self.path) + + def return_json(self): + self.return_dataframe() + self.json = self.df.to_json() diff --git a/sktalk/from_convokit.py b/sktalk/from_convokit.py new file mode 100644 index 0000000..aa242fa --- /dev/null +++ b/sktalk/from_convokit.py @@ -0,0 +1,2425 @@ +"""Documentation about the scikit-talk module - csv to json.""" +import json +import pandas as pd +from pandas import DataFrame +# import random +# import shutil +# from typing import Collection, Callable, Set, Generator, Tuple, ValuesView, Union +from typing import Optional, List, Dict, Union, Iterable, Callable, Generator +# from pandas import DataFrame +from tqdm import tqdm +from abc import ABCMeta, abstractmethod +from collections import defaultdict, deque +import os +from yaml import load, Loader + +try: + from collections.abc import MutableMapping +except: + from collections import MutableMapping + +# from convokit.convokitConfig import ConvoKitConfig +# from convokit.util import create_safe_id +# from .convoKitMatrix import ConvoKitMatrix +# from .corpusUtil import * +# from .corpus_helpers import * +# from .storageManager import StorageManager + + +### CLASSES + +class CorpusComponent: + def __init__( + self, + obj_type: str, + owner=None, + id=None, + initial_data=None, + vectors: List[str] = None, + meta=None, + ): + self.obj_type = obj_type # utterance, speaker, conversation + self._owner = owner + self._id = id + self.vectors = vectors if vectors is not None else [] + + # if the CorpusComponent is initialized with an owner set up an entry + # in the owner's storage; if it is not initialized with an owner + # (i.e. it is a standalone object) set up a dict-based temp storage + if self.owner is None: + self._temp_storage = initial_data if initial_data is not None else {} + else: + self.owner.storage.initialize_data_for_component( + self.obj_type, + self._id, + initial_value=(initial_data if initial_data is not None else {}), + ) + + if meta is None: + meta = dict() + self._meta = self.init_meta(meta) + + def get_owner(self): + return self._owner + + def set_owner(self, owner): + if owner is self._owner: + # no action needed + return + # stash the metadata first since reassigning self._owner will break its storage connection + meta_vals = {k: v for k, v in self.meta.items()} + previous_owner = self._owner + self._owner = owner + if owner is not None: + # when a new owner Corpus is assigned, we must take the following steps: + # (1) transfer this component's data to the new owner's StorageManager + # (2) avoid duplicates by removing the data from the old owner (or temp storage if there was no prior owner) + # (3) reinitialize the metadata instance + data_dict = ( + dict(previous_owner.storage.get_data(self.obj_type, self.id)) + if previous_owner is not None + else self._temp_storage + ) + self.owner.storage.initialize_data_for_component( + self.obj_type, self.id, initial_value=data_dict + ) + if previous_owner is not None: + previous_owner.storage.delete_data(self.obj_type, self.id) + previous_owner.storage.delete_data("meta", self.meta.storage_key) + else: + del self._temp_storage + self._meta = self.init_meta(meta_vals) + + owner = property(get_owner, set_owner) + + def init_meta(self, meta, overwrite=False): + if self._owner is None: + # ConvoKitMeta instances are not allowed for ownerless (standalone) + # components since they must be backed by a StorageManager. In this + # case we must forcibly convert the ConvoKitMeta instance to dict + if isinstance(meta, ConvoKitMeta): + meta = meta.to_dict() + return meta + else: + if isinstance(meta, ConvoKitMeta) and meta.owner is self._owner: + return meta + ck_meta = ConvoKitMeta(self, self.owner.meta_index, self.obj_type, overwrite=overwrite) + for key, value in meta.items(): + ck_meta[key] = value + return ck_meta + + def get_id(self): + return self._id + + def set_id(self, value): + if not isinstance(value, str) and value is not None: + self._id = str(value) + warn( + "{} id must be a string. ID input has been casted to a string.".format( + self.obj_type + ) + ) + else: + self._id = value + + id = property(get_id, set_id) + + def get_meta(self): + return self._meta + + def set_meta(self, new_meta): + self._meta = self.init_meta(new_meta, overwrite=True) + + meta = property(get_meta, set_meta) + + def get_data(self, property_name): + if self._owner is None: + return self._temp_storage[property_name] + return self.owner.storage.get_data(self.obj_type, self.id, property_name) + + def set_data(self, property_name, value): + if self._owner is None: + self._temp_storage[property_name] = value + else: + self.owner.storage.update_data(self.obj_type, self.id, property_name, value) + + # def __eq__(self, other): + # if type(self) != type(other): return False + # # do not compare 'utterances' and 'conversations' in Speaker.__dict__. recursion loop will occur. + # self_keys = set(self.__dict__).difference(['_owner', 'meta', 'utterances', 'conversations']) + # other_keys = set(other.__dict__).difference(['_owner', 'meta', 'utterances', 'conversations']) + # return self_keys == other_keys and all([self.__dict__[k] == other.__dict__[k] for k in self_keys]) + + def retrieve_meta(self, key: str): + """ + Retrieves a value stored under the key of the metadata of corpus object + :param key: name of metadata attribute + :return: value + """ + return self.meta.get(key, None) + + def add_meta(self, key: str, value) -> None: + """ + Adds a key-value pair to the metadata of the corpus object + :param key: name of metadata attribute + :param value: value of metadata attribute + :return: None + """ + self.meta[key] = value + + def get_vector( + self, vector_name: str, as_dataframe: bool = False, columns: Optional[List[str]] = None + ): + """ + Get the vector stored as `vector_name` for this object. + :param vector_name: name of vector + :param as_dataframe: whether to return the vector as a dataframe (True) or in its raw array form (False). False + by default. + :param columns: optional list of named columns of the vector to include. All columns returned otherwise. This + parameter is only used if as_dataframe is set to True + :return: a numpy / scipy array + """ + if vector_name not in self.vectors: + raise ValueError( + "This {} has no vector stored as '{}'.".format(self.obj_type, vector_name) + ) + + return self.owner.get_vector_matrix(vector_name).get_vectors( + ids=[self.id], as_dataframe=as_dataframe, columns=columns + ) + + def add_vector(self, vector_name: str): + """ + Logs in the Corpus component object's internal vectors list that the component object has a vector row + associated with it in the vector matrix named `vector_name`. + Transformers that add vectors to the Corpus should use this to update the relevant component objects during + the transform() step. + :param vector_name: name of vector matrix + :return: None + """ + if vector_name not in self.vectors: + self.vectors.append(vector_name) + + def has_vector(self, vector_name: str): + return vector_name in self.vectors + + def delete_vector(self, vector_name: str): + """ + Delete a vector associated with this Corpus component object. + :param vector_name: + :return: None + """ + self.vectors.remove(vector_name) + + def to_dict(self): + return { + "id": self.id, + "vectors": self.vectors, + "meta": self.meta if type(self.meta) == dict else self.meta.to_dict(), + } + + def __str__(self): + return "{}(id: {}, vectors: {}, meta: {})".format( + self.obj_type.capitalize(), self.id, self.vectors, self.meta + ) + + def __hash__(self): + return hash(self.obj_type + str(self.id)) + + def __repr__(self): + copy = self.__dict__.copy() + deleted_keys = [ + "utterances", + "conversations", + "user", + "_root", + "_utterance_ids", + "_speaker_ids", + ] + for k in deleted_keys: + if k in copy: + del copy[k] + + to_delete = [k for k in copy if k.startswith("_")] + to_add = {k[1:]: copy[k] for k in copy if k.startswith("_")} + + for k in to_delete: + del copy[k] + + copy.update(to_add) + + try: + return self.obj_type.capitalize() + "(" + str(copy) + ")" + except ( + AttributeError + ): # for backwards compatibility when corpus objects are saved as binary data, e.g. wikiconv + return "(" + str(copy) + ")" + + +class StorageManager(metaclass=ABCMeta): + """ + Abstraction layer for the concrete representation of data and metadata + within corpus components (e.g., Utterance text and timestamps). All requests + to access or modify corpusComponent fields (with the exception of ID) are + actually routed through one of StorageManager's concrete subclasses. Each + subclass implements a storage backend that contains the actual data. + """ + + def __init__(self): + # concrete data storage (i.e., collections) for each component type + # this will be assigned in subclasses + self.data = {"utterance": None, "conversation": None, "speaker": None, "meta": None} + + @abstractmethod + def get_collection_ids(self, component_type: str): + """ + Returns a list of all object IDs within the component_type collection + """ + return NotImplemented + + @abstractmethod + def has_data_for_component(self, component_type: str, component_id: str) -> bool: + """ + Check if there is an existing entry for the component of type component_type + with id component_id + """ + return NotImplemented + + @abstractmethod + def initialize_data_for_component( + self, component_type: str, component_id: str, overwrite: bool = False, initial_value=None + ): + """ + Create a blank entry for a component of type component_type with id + component_id. Will avoid overwriting any existing data unless the + overwrite parameter is set to True. + """ + return NotImplemented + + @abstractmethod + def get_data( + self, + component_type: str, + component_id: str, + property_name: Optional[str] = None, + index=None, + ): + """ + Retrieve the property data for the component of type component_type with + id component_id. If property_name is specified return only the data for + that property, otherwise return the dict containing all properties. + Additionally, the expected type of the property to be fetched may be specified + as a string; this is meant to be used for metadata in conjunction with the index. + """ + return NotImplemented + + @abstractmethod + def update_data( + self, + component_type: str, + component_id: str, + property_name: str, + new_value, + index=None, + ): + """ + Set or update the property data for the component of type component_type + with id component_id. For metadata, the Python object type may also be + specified, to be used in conjunction with the index. + """ + return NotImplemented + + @abstractmethod + def delete_data( + self, component_type: str, component_id: str, property_name: Optional[str] = None + ): + """ + Delete a data entry from this StorageManager for the component of type + component_type with id component_id. If property_name is specified + delete only that property, otherwise delete the entire entry. + """ + return NotImplemented + + @abstractmethod + def clear_all_data(self): + """ + Erase all data from this StorageManager (i.e., reset self.data to its + initial empty state; Python will garbage-collect the now-unreferenced + old data entries). This is used for cleanup after destructive Corpus + operations. + """ + return NotImplemented + + @abstractmethod + def count_entries(self, component_type: str): + """ + Count the number of entries held for the specified component type by + this StorageManager instance + """ + return NotImplemented + + def get_collection(self, component_type: str): + if component_type not in self.data: + raise ValueError( + 'component_type must be one of "utterance", "conversation", "speaker", or "meta".' + ) + return self.data[component_type] + + def purge_obsolete_entries(self, utterance_ids, conversation_ids, speaker_ids, meta_ids): + """ + Compare the entries in this StorageManager to the existing component ids + provided as parameters, and delete any entries that are not found in the + parameter ids. + """ + ref_ids = { + "utterance": set(utterance_ids), + "conversation": set(conversation_ids), + "speaker": set(speaker_ids), + "meta": set(meta_ids), + } + for obj_type in self.data: + for obj_id in self.get_collection_ids(obj_type): + if obj_id not in ref_ids[obj_type]: + self.delete_data(obj_type, obj_id) + +class Speaker(CorpusComponent): + """ + Represents a single speaker in a dataset. + :param id: id of the speaker. + :type id: str + :param utts: dictionary of utterances by the speaker, where key is utterance id + :param convos: dictionary of conversations started by the speaker, where key is conversation id + :param meta: arbitrary dictionary of attributes associated + with the speaker. + :type meta: dict + :ivar id: id of the speaker. + :ivar meta: A dictionary-like view object providing read-write access to + speaker-level metadata. + """ + + def __init__( + self, + owner=None, + id: str = None, + utts=None, + convos=None, + meta: Optional[Dict] = None, + ): + super().__init__(obj_type="speaker", owner=owner, id=id, meta=meta) + self.utterances = utts if utts is not None else dict() + self.conversations = convos if convos is not None else dict() + # self._split_attribs = set() + # self._update_uid() + + # def identify_by_attribs(self, attribs: Collection) -> None: + # """Identify a speaker by a list of attributes. Sets which speaker info + # attributes should distinguish speakers of the same name in equality tests. + # For example, in the Supreme Court dataset, speakers are labeled with the + # current case id. Call this method with attribs = ["case"] to count + # the same person across different cases as different speakers. + # + # By default, if this function is not called, speakers are identified by name only. + # + # :param attribs: Collection of attribute names. + # :type attribs: Collection + # """ + # + # self._split_attribs = set(attribs) + # # self._update_uid() + + def _add_utterance(self, utt): + self.utterances[utt.id] = utt + + def _add_conversation(self, convo): + self.conversations[convo.id] = convo + + def get_utterance(self, ut_id: str): # -> Utterance: + """ + Get the Utterance with the specified Utterance id + :param ut_id: The id of the Utterance + :return: An Utterance object + """ + return self.utterances[ut_id] + + def iter_utterances(self, selector=lambda utt: True): # -> Generator[Utterance, None, None]: + """ + Get utterances made by the Speaker, with an optional selector that selects for Utterances that + should be included. + :param selector: a (lambda) function that takes an Utterance and returns True or False (i.e. include / exclude). + By default, the selector includes all Utterances in the Corpus. + :return: An iterator of the Utterances made by the speaker + """ + for v in self.utterances.values(): + if selector(v): + yield v + + def get_utterances_dataframe(self, selector=lambda utt: True, exclude_meta: bool = False): + """ + Get a DataFrame of the Utterances made by the Speaker with fields and metadata attributes. + Set an optional selector that filters for Utterances that should be included. + Edits to the DataFrame do not change the corpus in any way. + :param exclude_meta: whether to exclude metadata + :param selector: a (lambda) function that takes a Utterance and returns True or False (i.e. include / exclude). + By default, the selector includes all Utterances in the Corpus. + :return: a pandas DataFrame + """ + return get_utterances_dataframe(self, selector, exclude_meta) + + def get_utterance_ids(self, selector=lambda utt: True) -> List[str]: + """ + :return: a List of the ids of Utterances made by the speaker + """ + return list([utt.id for utt in self.iter_utterances(selector)]) + + def get_conversation(self, cid: str): # -> Conversation: + """ + Get the Conversation with the specified Conversation id + :param cid: The id of the Conversation + :return: A Conversation object + """ + return self.conversations[cid] + + def iter_conversations( + self, selector=lambda convo: True + ): # -> Generator[Conversation, None, None]: + """ + :return: An iterator of the Conversations that the speaker has participated in + """ + for v in self.conversations.values(): + if selector(v): + yield v + + def get_conversations_dataframe(self, selector=lambda convo: True, exclude_meta: bool = False): + """ + Get a DataFrame of the Conversations the Speaker has participated in, with fields and metadata attributes. + Set an optional selector that filters for Conversations that should be included. Edits to the DataFrame do not + change the corpus in any way. + :param exclude_meta: whether to exclude metadata + :param selector: a (lambda) function that takes a Conversation and returns True or False (i.e. include / exclude). + By default, the selector includes all Conversations in the Corpus. + :return: a pandas DataFrame + """ + return get_conversations_dataframe(self, selector, exclude_meta) + + def get_conversation_ids(self, selector=lambda convo: True) -> List[str]: + """ + :return: a List of the ids of Conversations started by the speaker + """ + return [convo.id for convo in self.iter_conversations(selector)] + + def print_speaker_stats(self): + """ + Helper function for printing the number of Utterances made and Conversations participated in by the Speaker. + :return: None (prints output) + """ + print("Number of Utterances: {}".format(len(list(self.iter_utterances())))) + print("Number of Conversations: {}".format(len(list(self.iter_conversations())))) + + def __lt__(self, other): + return self.id < other.id + + def __hash__(self): + return super().__hash__() + + def __eq__(self, other): + if not isinstance(other, Speaker): + return False + try: + return self.id == other.id + except AttributeError: + return self.__dict__["_name"] == other.__dict__["_name"] + + def __str__(self): + return "Speaker(id: {}, vectors: {}, meta: {})".format( + repr(self.id), self.vectors, self.meta + ) + + +class Utterance(CorpusComponent): + """Represents a single utterance in the dataset. + :param id: the unique id of the utterance. + :param speaker: the speaker giving the utterance. + :param conversation_id: the id of the root utterance of the conversation. + :param reply_to: id of the utterance this was a reply to. + :param timestamp: timestamp of the utterance. Can be any + comparable type. + :param text: text of the utterance. + :ivar id: the unique id of the utterance. + :ivar speaker: the speaker giving the utterance. + :ivar conversation_id: the id of the root utterance of the conversation. + :ivar reply_to: id of the utterance this was a reply to. + :ivar timestamp: timestamp of the utterance. + :ivar text: text of the utterance. + :ivar meta: A dictionary-like view object providing read-write access to + utterance-level metadata. + """ + + def __init__( + self, + owner=None, + id: Optional[str] = None, + speaker: Optional[Speaker] = None, + conversation_id: Optional[str] = None, + reply_to: Optional[str] = None, + timestamp: Optional[int] = None, + text: str = "", + meta: Optional[Dict] = None, + ): + # check arguments that have alternate naming due to backwards compatibility + if speaker is None: + raise ValueError("No Speaker found: Utterance must be initialized with a Speaker.") + + if conversation_id is not None and not isinstance(conversation_id, str): + warn( + "Utterance conversation_id must be a string: conversation_id of utterance with ID: {} " + "has been casted to a string.".format(id) + ) + conversation_id = str(conversation_id) + if not isinstance(text, str): + warn( + "Utterance text must be a string: text of utterance with ID: {} " + "has been casted to a string.".format(id) + ) + text = "" if text is None else str(text) + + props = { + "speaker_id": speaker.id, + "conversation_id": conversation_id, + "reply_to": reply_to, + "timestamp": timestamp, + "text": text, + } + super().__init__(obj_type="utterance", owner=owner, id=id, initial_data=props, meta=meta) + self.speaker_ = speaker + + ############################################################################ + ## directly-accessible class properties (roughly equivalent to keys in the + ## JSON, plus aliases for compatibility) + ############################################################################ + + def _get_speaker(self): + return self.speaker_ + + def _set_speaker(self, val): + self.speaker_ = val + self.set_data("speaker_id", self.speaker.id) + + speaker = property(_get_speaker, _set_speaker) + + def _get_conversation_id(self): + return self.get_data("conversation_id") + + def _set_conversation_id(self, val): + self.set_data("conversation_id", val) + + conversation_id = property(_get_conversation_id, _set_conversation_id) + + def _get_reply_to(self): + return self.get_data("reply_to") + + def _set_reply_to(self, val): + self.set_data("reply_to", val) + + reply_to = property(_get_reply_to, _set_reply_to) + + def _get_timestamp(self): + return self.get_data("timestamp") + + def _set_timestamp(self, val): + self.set_data("timestamp", val) + + timestamp = property(_get_timestamp, _set_timestamp) + + def _get_text(self): + return self.get_data("text") + + def _set_text(self, val): + self.set_data("text", val) + + text = property(_get_text, _set_text) + + ############################################################################ + ## end properties + ############################################################################ + + def get_conversation(self): + """ + Get the Conversation (identified by Utterance.conversation_id) this Utterance belongs to + :return: a Conversation object + """ + return self.owner.get_conversation(self.conversation_id) + + def get_speaker(self): + """ + Get the Speaker that made this Utterance. + :return: a Speaker object + """ + + return self.speaker + + def to_dict(self): + return { + "id": self.id, + "conversation_id": self.conversation_id, + "reply_to": self.reply_to, + "speaker": self.speaker, + "timestamp": self.timestamp, + "text": self.text, + "vectors": self.vectors, + "meta": self.meta if type(self.meta) == dict else self.meta.to_dict(), + } + + def __hash__(self): + return super().__hash__() + + def __eq__(self, other): + if not isinstance(other, Utterance): + return False + try: + return ( + self.id == other.id + and ( + self.conversation_id is None + or other.conversation_id is None + or self.conversation_id == other.conversation_id + ) + and self.reply_to == other.reply_to + and self.speaker == other.speaker + and self.timestamp == other.timestamp + and self.text == other.text + ) + except AttributeError: # for backwards compatibility with wikiconv + return self.__dict__ == other.__dict__ + + def __str__(self): + return ( + "Utterance(id: {}, conversation_id: {}, reply-to: {}, " + "speaker: {}, timestamp: {}, text: {}, vectors: {}, meta: {})".format( + repr(self.id), + self.conversation_id, + self.reply_to, + self.speaker, + self.timestamp, + repr(self.text), + self.vectors, + self.meta, + ) + ) + + +class ConvoKitMeta(MutableMapping, dict): + """ + ConvoKitMeta is a dictlike object that stores the metadata attributes of a corpus component + """ + + def __init__(self, owner, convokit_index, obj_type, overwrite=False): + self.owner = owner # Corpus or CorpusComponent + self.index: ConvoKitIndex = convokit_index + self.obj_type = obj_type + + self._get_storage().initialize_data_for_component( + "meta", self.storage_key, overwrite=overwrite + ) + + @property + def storage_key(self) -> str: + return f"{self.obj_type}_{self.owner.id}" + + def __getitem__(self, item): + return self._get_storage().get_data( + "meta", self.storage_key, item, self.index.get_index(self.obj_type) + ) + + def _get_storage(self): + # special case for Corpus meta since that's the only time owner is not a CorpusComponent + # since cannot directly import Corpus to check the type (circular import), as a proxy we + # check for the obj_type attribute which is common to all CorpusComponent but not + # present in Corpus + if not hasattr(self.owner, "obj_type"): + return self.owner.storage + # self.owner -> CorpusComponent + # self.owner.owner -> Corpus that owns the CorpusComponent (only Corpus has direct pointer to storage) + return self.owner.owner.storage + + @staticmethod + def _check_type_and_update_index(index, obj_type, key, value): + if key not in index.indices[obj_type]: + if isinstance(value, type(None)): # new entry with None type means can't infer type yet + index.create_new_index(obj_type, key=key) + else: + type_ = _optimized_type_check(value) + index.update_index(obj_type, key=key, class_type=type_) + else: + # entry exists + if not isinstance(value, type(None)): # do not update index if value is None + if index.get_index(obj_type)[key] != ["bin"]: # if "bin" do no further checks + if str(type(value)) not in index.get_index(obj_type)[key]: + new_type = _optimized_type_check(value) + + if new_type == "bin": + index.set_index(obj_type, key, "bin") + else: + index.update_index(obj_type, key, new_type) + + def __setitem__(self, key, value): + if not isinstance(key, str): + warn("Metadata attribute keys must be strings. Input key has been casted to a string.") + key = str(key) + + if self.index.type_check: + ConvoKitMeta._check_type_and_update_index(self.index, self.obj_type, key, value) + self._get_storage().update_data( + "meta", self.storage_key, key, value, self.index.get_index(self.obj_type) + ) + + def __delitem__(self, key): + if self.obj_type == "corpus": + self.index.del_from_index(self.obj_type, key) + self._get_storage().delete_data("meta", self.storage_key, key) + else: + if self.index.lock_metadata_deletion[self.obj_type]: + warn( + "For consistency in metadata attributes in Corpus component objects, deleting metadata attributes " + "from component objects individually is not allowed. " + "To delete this metadata attribute from all Corpus components of this type, " + "use corpus.delete_metadata(obj_type='{}', attribute='{}') instead.".format( + self.obj_type, key + ) + ) + else: + self._get_storage().delete_data("meta", self.storage_key, key) + + def __iter__(self): + return ( + self._get_storage() + .get_data("meta", self.storage_key, index=self.index.get_index(self.obj_type)) + .__iter__() + ) + + def __len__(self): + return ( + self._get_storage() + .get_data("meta", self.storage_key, index=self.index.get_index(self.obj_type)) + .__len__() + ) + + def __contains__(self, x): + return ( + self._get_storage() + .get_data("meta", self.storage_key, index=self.index.get_index(self.obj_type)) + .__contains__(x) + ) + + def __repr__(self) -> str: + return "ConvoKitMeta(" + self.to_dict().__repr__() + ")" + + def to_dict(self): + return dict( + self._get_storage().get_data( + "meta", self.storage_key, index=self.index.get_index(self.obj_type) + ) + ) + + def reinitialize_from(self, other: Union["ConvoKitMeta", dict]): + """ + Reinitialize this ConvoKitMeta instance with the data from other + """ + if isinstance(other, ConvoKitMeta): + other = {k: v for k, v in other.to_dict().items()} + elif not isinstance(other, dict): + raise TypeError( + "ConvoKitMeta can only be reinitialized from a dict instance or another ConvoKitMeta" + ) + self._get_storage().initialize_data_for_component( + "meta", self.storage_key, overwrite=True, initial_value=other + ) + + +# _basic_types = {type(0), type(1.0), type("str"), type(True)} # cannot include lists or dicts + + +# def _optimized_type_check(val): +# # if type(obj) +# if type(val) in _basic_types: +# return str(type(val)) +# else: +# try: +# json.dumps(val) +# return str(type(val)) +# except (TypeError, OverflowError): +# return "bin" + +DEFAULT_CONFIG_CONTENTS = ( + "# Default Storage Parameters\n" + "db_host: localhost:27017\n" + "data_directory: ~/.convokit/saved-corpora\n" + "default_storage_mode: mem" +) + +ENV_VARS = {"db_host": "CONVOKIT_DB_HOST", "default_storage_mode": "CONVOKIT_STORAGE_MODE"} + + +class ConvoKitConfig: + """ + Utility class providing read-only access to the ConvoKit config file + """ + + def __init__(self, filename: Optional[str] = None): + if filename is None: + filename = os.path.expanduser("~/.convokit/config.yml") + + if not os.path.isfile(filename): + convo_dir = os.path.dirname(filename) + if not os.path.isdir(convo_dir): + os.makedirs(convo_dir) + with open(filename, "w") as f: + print( + f"No configuration file found at {filename}; writing with contents: \n{DEFAULT_CONFIG_CONTENTS}" + ) + f.write(DEFAULT_CONFIG_CONTENTS) + self.config_contents = load(DEFAULT_CONFIG_CONTENTS, Loader=Loader) + else: + with open(filename, "r") as f: + self.config_contents = load(f.read(), Loader=Loader) + + def _get_config_from_env_or_file(self, config_key: str, default_val): + env_val = os.environ.get(ENV_VARS[config_key], None) + if env_val is not None: + # environment variable setting takes priority + return env_val + return self.config_contents.get(config_key, default_val) + + @property + def db_host(self): + return self._get_config_from_env_or_file("db_host", "localhost:27017") + + @property + def data_directory(self): + return self.config_contents.get("data_directory", "~/.convokit/saved-corpora") + + @property + def default_storage_mode(self): + return self._get_config_from_env_or_file("default_storage_mode", "mem") + +class Conversation(CorpusComponent): + """ + Represents a discrete subset of utterances in the dataset, connected by a reply-to chain. + :param owner: The Corpus that this Conversation belongs to + :param id: The unique ID of this Conversation + :param utterances: A list of the IDs of the Utterances in this Conversation + :param meta: Table of initial values for conversation-level metadata + :ivar id: the ID of the Conversation + :ivar meta: A dictionary-like view object providing read-write access to + conversation-level metadata. + """ + + def __init__( + self, + owner, + id: Optional[str] = None, + utterances: Optional[List[str]] = None, + meta: Optional[Dict] = None, + ): + super().__init__(obj_type="conversation", owner=owner, id=id, meta=meta) + self._owner = owner + self._utterance_ids: List[str] = utterances + self._speaker_ids = None + self.tree: Optional[UtteranceNode] = None + + def _add_utterance(self, utt: Utterance): + self._utterance_ids.append(utt.id) + self._speaker_ids = None + self.tree = None + + def get_utterance_ids(self) -> List[str]: + """Produces a list of the unique IDs of all utterances in the + Conversation, which can be used in calls to get_utterance() to retrieve + specific utterances. Provides no ordering guarantees for the list. + :return: a list of IDs of Utterances in the Conversation + """ + # pass a copy of the list + return self._utterance_ids[:] + + def get_utterance(self, ut_id: str) -> Utterance: + """Looks up the Utterance associated with the given ID. Raises a + KeyError if no utterance by that ID exists. + :return: the Utterance with the given ID + """ + # delegate to the owner Corpus since Conversation does not itself own + # any Utterances + return self._owner.get_utterance(ut_id) + + def iter_utterances( + self, selector: Callable[[Utterance], bool] = lambda utt: True + ) -> Generator[Utterance, None, None]: + """ + Get utterances in the Corpus, with an optional selector that filters for Utterances that should be included. + :param selector: a (lambda) function that takes an Utterance and returns True or False (i.e. include / exclude). + By default, the selector includes all Utterances in the Conversation. + :return: a generator of Utterances + """ + for ut_id in self._utterance_ids: + utt = self._owner.get_utterance(ut_id) + if selector(utt): + yield utt + + def get_utterances_dataframe( + self, selector: Callable[[Utterance], bool] = lambda utt: True, exclude_meta: bool = False + ): + """ + Get a DataFrame of the Utterances in the Conversation with fields and metadata attributes. + Set an optional selector that filters for Utterances that should be included. + Edits to the DataFrame do not change the corpus in any way. + :param selector: a (lambda) function that takes an Utterance and returns True or False (i.e. include / exclude). + By default, the selector includes all Utterances in the Conversation. + :param exclude_meta: whether to exclude metadata + :return: a pandas DataFrame + """ + return get_utterances_dataframe(self, selector, exclude_meta) + + def get_speaker_ids(self) -> List[str]: + """ + Produces a list of ids of all speakers in the Conversation, which can be used in calls to get_speaker() + to retrieve specific speakers. Provides no ordering guarantees for the list. + :return: a list of speaker ids + """ + if self._speaker_ids is None: + # first call to get_speaker_ids or iter_speakers; precompute cached list of speaker ids + self._speaker_ids = set() + for ut_id in self._utterance_ids: + ut = self._owner.get_utterance(ut_id) + self._speaker_ids.add(ut.speaker.id) + return list(self._speaker_ids) + + def get_speaker(self, speaker_id: str) -> Speaker: + """ + Looks up the Speaker with the given name. Raises a KeyError if no speaker + with that name exists. + :return: the Speaker with the given speaker_id + """ + # delegate to the owner Corpus since Conversation does not itself own + # any Utterances + return self._owner.get_speaker(speaker_id) + + def iter_speakers( + self, selector: Callable[[Speaker], bool] = lambda speaker: True + ) -> Generator[Speaker, None, None]: + """ + Get Speakers that have participated in the Conversation, with an optional selector that filters for Speakers + that should be included. + :param selector: a (lambda) function that takes a Speaker and returns True or False (i.e. include / exclude). + By default, the selector includes all Speakers in the Conversation. + :return: a generator of Speakers + """ + if self._speaker_ids is None: + # first call to get_ids or iter_speakers; precompute cached list of speaker ids + self._speaker_ids = set() + for ut_id in self._utterance_ids: + ut = self._owner.get_utterance(ut_id) + self._speaker_ids.add(ut.speaker.id) + for speaker_id in self._speaker_ids: + speaker = self._owner.get_speaker(speaker_id) + if selector(speaker): + yield speaker + + def get_speakers_dataframe( + self, + selector: Optional[Callable[[Speaker], bool]] = lambda utt: True, + exclude_meta: bool = False, + ): + """ + Get a DataFrame of the Speakers that have participated in the Conversation with fields and metadata attributes, + with an optional selector that filters Speakers that should be included. + Edits to the DataFrame do not change the corpus in any way. + :param exclude_meta: whether to exclude metadata + :param selector: selector: a (lambda) function that takes a Speaker and returns True or False + (i.e. include / exclude). By default, the selector includes all Speakers in the Conversation. + :return: a pandas DataFrame + """ + return get_speakers_dataframe(self, selector, exclude_meta) + + def print_conversation_stats(self): + """ + Helper function for printing the number of Utterances and Spekaers in the Conversation. + :return: None (prints output) + """ + print("Number of Utterances: {}".format(len(list(self.iter_utterances())))) + print("Number of Speakers: {}".format(len(list(self.iter_speakers())))) + + def get_chronological_speaker_list( + self, selector: Callable[[Speaker], bool] = lambda speaker: True + ): + """ + Get the speakers in the conversation sorted in chronological order (speakers may appear more than once) + :param selector: (lambda) function for which speakers should be included; all speakers are included by default + :return: list of speakers for each chronological utterance + """ + try: + chrono_utts = sorted(list(self.iter_utterances()), key=lambda utt: utt.timestamp) + return [utt.speaker for utt in chrono_utts if selector(utt.speaker)] + except TypeError as e: + raise ValueError(str(e) + "\nUtterance timestamps may not have been set correctly.") + + def check_integrity(self, verbose: bool = True) -> bool: + """ + Check the integrity of this Conversation; i.e. do the constituent utterances form a complete reply-to chain? + :param verbose: whether to print errors indicating the problems with the Conversation + :return: True if the conversation structure is complete else False + """ + if verbose: + print("Checking reply-to chain of Conversation", self.id) + utt_reply_tos = {utt.id: utt.reply_to for utt in self.iter_utterances()} + target_utt_ids = set(list(utt_reply_tos.values())) + speaker_utt_ids = set(list(utt_reply_tos.keys())) + root_utt_id = target_utt_ids - speaker_utt_ids # There should only be 1 root_utt_id: None + + if len(root_utt_id) != 1: + if verbose: + for utt_id in root_utt_id: + if utt_id is not None: + warn("ERROR: Missing utterance {}".format(utt_id)) + return False + else: + root_id = list(root_utt_id)[0] + if root_id is not None: + if verbose: + warn("ERROR: Missing utterance {}".format(root_id)) + return False + + # sanity check + utts_replying_to_none = 0 + for utt in self.iter_utterances(): + if utt.reply_to is None: + utts_replying_to_none += 1 + + if utts_replying_to_none > 1: + if verbose: + warn("ERROR: Found more than one Utterance replying to None.") + return False + + circular = [ + utt_id for utt_id, utt_reply_to in utt_reply_tos.items() if utt_id == utt_reply_to + ] + if len(circular) > 0: + if verbose: + warn( + "ERROR: Found utterances with .reply_to pointing to themselves: {}".format( + circular + ) + ) + return False + + if verbose: + print("No issues found.\n") + return True + + def initialize_tree_structure(self): + if not self.check_integrity(verbose=False): + raise ValueError( + "Conversation {} reply-to chain does not form a valid tree.".format(self.id) + ) + + root_node_id = None + # Find root node + for utt in self.iter_utterances(): + if utt.reply_to is None: + root_node_id = utt.id + + parent_to_children_ids = defaultdict(list) + for utt in self.iter_utterances(): + parent_to_children_ids[utt.reply_to].append(utt.id) + + wrapped_utts = {utt.id: UtteranceNode(utt) for utt in self.iter_utterances()} + + for parent_id, wrapped_utt in wrapped_utts.items(): + wrapped_utt.set_children( + [wrapped_utts[child_id] for child_id in parent_to_children_ids[parent_id]] + ) + + self.tree = wrapped_utts[root_node_id] + + def traverse(self, traversal_type: str, as_utterance: bool = True): + """ + Traverse through the Conversation tree structure in a breadth-first search ('bfs'), depth-first search (dfs), + pre-order ('preorder'), or post-order ('postorder') way. + :param traversal_type: dfs, bfs, preorder, or postorder + :param as_utterance: whether the iterator should yield the utterance (True) or the utterance node (False) + :return: an iterator of the utterances or utterance nodes + """ + if self.tree is None: + self.initialize_tree_structure() + if self.tree is None: + raise ValueError( + "Failed to traverse because Conversation reply-to chain does not form a valid tree." + ) + + traversals = { + "bfs": self.tree.bfs_traversal, + "dfs": self.tree.dfs_traversal, + "preorder": self.tree.pre_order, + "postorder": self.tree.post_order, + } + + for utt_node in traversals[traversal_type](): + yield utt_node.utt if as_utterance else utt_node + + def get_subtree(self, root_utt_id): + """ + Get the utterance node of the specified input id + :param root_utt_id: id of the root node that the subtree starts from + :return: UtteranceNode object + """ + if self.tree is None: + self.initialize_tree_structure() + if self.tree is None: + raise ValueError( + "Failed to traverse because Conversation reply-to chain does not form a valid tree." + ) + + for utt_node in self.tree.bfs_traversal(): + if utt_node.utt.id == root_utt_id: + return utt_node + + def get_longest_paths(self) -> List[List[Utterance]]: + """ + Finds the Utterances form the longest path (i.e. root to leaf) in the Conversation tree. + If there are multiple paths with tied lengths, returns all of them as a list of lists. If only one such path + exists, a list containing a single list of Utterances is returned. + :return: a list of lists of Utterances + """ + if self.tree is None: + self.initialize_tree_structure() + if self.tree is None: + raise ValueError( + "Failed to traverse because Conversation reply-to chain does not form a valid tree." + ) + + paths = self.get_root_to_leaf_paths() + max_len = max(len(path) for path in paths) + + return [p for p in paths if len(p) == max_len] + + def _print_convo_helper( + self, + root: str, + indent: int, + reply_to_dict: Dict[str, str], + utt_info_func: Callable[[Utterance], str], + limit=None, + ) -> None: + """ + Helper function for print_conversation_structure() + """ + if limit is not None: + if self.get_utterance(root).meta["order"] > limit: + return + print(" " * indent + utt_info_func(self.get_utterance(root))) + children_utt_ids = [k for k, v in reply_to_dict.items() if v == root] + for child_utt_id in children_utt_ids: + self._print_convo_helper( + root=child_utt_id, + indent=indent + 4, + reply_to_dict=reply_to_dict, + utt_info_func=utt_info_func, + limit=limit, + ) + + def print_conversation_structure( + self, + utt_info_func: Callable[[Utterance], str] = lambda utt: utt.speaker.id, + limit: int = None, + ) -> None: + """ + Prints an indented representation of utterances in the Conversation with conversation reply-to structure + determining the indented level. The details of each utterance to be printed can be configured. + If limit is set to a value other than None, this will annotate utterances with an 'order' metadata indicating + their temporal order in the conversation, where the first utterance in the conversation is annotated with 1. + :param utt_info_func: callable function taking an utterance as input and returning a string of the desired + utterance information. By default, this is a lambda function returning the utterance's speaker's id + :param limit: maximum number of utterances to print out. if k, this includes the first k utterances. + :return: None. Prints to stdout. + """ + if not self.check_integrity(verbose=False): + raise ValueError( + "Could not print conversation structure: The utterance reply-to chain is broken. " + "Try check_integrity() to diagnose the problem." + ) + + if limit is not None: + assert isinstance(limit, int) + for idx, utt in enumerate(self.get_chronological_utterance_list()): + utt.meta["order"] = idx + 1 + + root_utt_id = [utt for utt in self.iter_utterances() if utt.reply_to is None][0].id + reply_to_dict = {utt.id: utt.reply_to for utt in self.iter_utterances()} + + self._print_convo_helper( + root=root_utt_id, + indent=0, + reply_to_dict=reply_to_dict, + utt_info_func=utt_info_func, + limit=limit, + ) + + def get_utterances_dataframe(self, selector=lambda utt: True, exclude_meta: bool = False): + """ + Get a DataFrame of the Utterances in the COnversation with fields and metadata attributes. + Set an optional selector that filters Utterances that should be included. + Edits to the DataFrame do not change the corpus in any way. + :param exclude_meta: whether to exclude metadata + :param selector: a (lambda) function that takes a Utterance and returns True or False (i.e. include / exclude). + By default, the selector includes all Utterances in the Conversation. + :return: a pandas DataFrame + """ + return get_utterances_dataframe(self, selector, exclude_meta) + + def get_chronological_utterance_list( + self, selector: Callable[[Utterance], bool] = lambda utt: True + ): + """ + Get the utterances in the conversation sorted in increasing order of timestamp + :param selector: function for which utterances should be included; all utterances are included by default + :return: list of utterances, sorted by timestamp + """ + try: + return sorted( + [utt for utt in self.iter_utterances(selector)], key=lambda utt: utt.timestamp + ) + except TypeError as e: + raise ValueError(str(e) + "\nUtterance timestamps may not have been set correctly.") + + def _get_path_from_leaf_to_root( + self, leaf_utt: Utterance, root_utt: Utterance + ) -> List[Utterance]: + """ + Helper function for get_root_to_leaf_paths, which returns the path for a given leaf_utt and root_utt + """ + if leaf_utt == root_utt: + return [leaf_utt] + path = [leaf_utt] + root_id = root_utt.id + while leaf_utt.reply_to != root_id: + path.append(self.get_utterance(leaf_utt.reply_to)) + leaf_utt = path[-1] + path.append(root_utt) + return path[::-1] + + def get_root_to_leaf_paths(self) -> List[List[Utterance]]: + """ + Get the paths (stored as a list of lists of utterances) from the root to each of the leaves + in the conversational tree + :return: List of lists of Utterances + """ + if not self.check_integrity(verbose=False): + raise ValueError( + "Conversation failed integrity check. " + "It is either missing an utterance in the reply-to chain and/or has multiple root nodes. " + "Run check_integrity() to diagnose issues." + ) + + utt_reply_tos = {utt.id: utt.reply_to for utt in self.iter_utterances()} + target_utt_ids = set(list(utt_reply_tos.values())) + speaker_utt_ids = set(list(utt_reply_tos.keys())) + root_utt_id = target_utt_ids - speaker_utt_ids # There should only be 1 root_utt_id: None + assert len(root_utt_id) == 1 + root_utt = [utt for utt in self.iter_utterances() if utt.reply_to is None][0] + leaf_utt_ids = speaker_utt_ids - target_utt_ids + + paths = [ + self._get_path_from_leaf_to_root(self.get_utterance(leaf_utt_id), root_utt) + for leaf_utt_id in leaf_utt_ids + ] + return paths + + @staticmethod + def generate_default_conversation_id(utterance_id): + return f"__default_conversation__{utterance_id}" + + def __hash__(self): + return super().__hash__() + + def __eq__(self, other): + if not isinstance(other, Conversation): + return False + return self.id == other.id and set(self._utterance_ids) == set(other._utterance_ids) + + def __str__(self): + return "Conversation('id': {}, 'utterances': {}, 'meta': {})".format( + repr(self.id), self._utterance_ids, self.meta + ) + +class Corpus: + """ + Represents a dataset, which can be loaded from a folder or constructed from a list of utterances. + :param filename: Path to a folder containing a Corpus or to an utterances.jsonl / utterances.json file to load + :param utterances: list of utterances to initialize Corpus from + :param preload_vectors: list of names of vectors to be preloaded from directory; by default, + no vectors are loaded but can be loaded any time after corpus initialization (i.e. vectors are lazy-loaded). + :param utterance_start_index: if loading from directory and the corpus folder contains utterances.jsonl, specify the + line number (zero-indexed) to begin parsing utterances from + :param utterance_end_index: if loading from directory and the corpus folder contains utterances.jsonl, specify the + line number (zero-indexed) of the last utterance to be parsed. + :param merge_lines: whether to merge adjacent lines from same speaker if multiple consecutive utterances belong to + the same conversation. + :param exclude_utterance_meta: utterance metadata to be ignored + :param exclude_conversation_meta: conversation metadata to be ignored + :param exclude_speaker_meta: speaker metadata to be ignored + :param exclude_overall_meta: overall metadata to be ignored + :param disable_type_check: whether to do type checking when loading the Corpus from a directory. + Type-checking ensures that the ConvoKitIndex is initialized correctly. However, it may be unnecessary if the + index.json is already accurate and disabling it will allow for a faster corpus load. This parameter is set to + True by default, i.e. type-checking is not carried out. + :ivar meta_index: index of Corpus metadata + :ivar vectors: the vectors stored in the Corpus + :ivar corpus_dirpath: path to the directory the corpus was loaded from + """ + + def __init__( + self, + filename: Optional[str] = None, + utterances: Optional[List[Utterance]] = None, + db_collection_prefix: Optional[str] = None, + db_host: Optional[str] = None, + preload_vectors: List[str] = None, + utterance_start_index: int = None, + utterance_end_index: int = None, + merge_lines: bool = False, + exclude_utterance_meta: Optional[List[str]] = None, + exclude_conversation_meta: Optional[List[str]] = None, + exclude_speaker_meta: Optional[List[str]] = None, + exclude_overall_meta: Optional[List[str]] = None, + disable_type_check=True, + storage_type: Optional[str] = None, + storage: Optional[StorageManager] = None, + ): + self.config = ConvoKitConfig() + self.corpus_dirpath = get_corpus_dirpath(filename) + + # configure corpus ID (optional for mem mode, required for DB mode) + if storage_type is None: + storage_type = self.config.default_storage_mode + if db_collection_prefix is None and filename is None and storage_type == "db": + db_collection_prefix = create_safe_id() + warn( + "You are in DB mode, but no collection prefix was specified and no filename was given from which to infer one." + "Will use a randomly generated unique prefix " + db_collection_prefix + ) + self.id = get_corpus_id(db_collection_prefix, filename, storage_type) + self.storage_type = storage_type + self.storage = initialize_storage(self, storage, storage_type, db_host) + + self.meta_index = ConvoKitIndex(self) + self.meta = ConvoKitMeta(self, self.meta_index, "corpus") + + # private storage + self._vector_matrices = dict() + + convos_data = defaultdict(dict) + if exclude_utterance_meta is None: + exclude_utterance_meta = [] + if exclude_conversation_meta is None: + exclude_conversation_meta = [] + if exclude_speaker_meta is None: + exclude_speaker_meta = [] + if exclude_overall_meta is None: + exclude_overall_meta = [] + + if filename is not None and storage_type == "db": + # JSON-to-DB construction mode uses a specialized code branch, which + # optimizes for this use case by using direct batch insertions into the + # DB rather than going through the StorageManager, hence improving + # efficiency. + + with open(os.path.join(filename, "index.json"), "r") as f: + idx_dict = json.load(f) + self.meta_index.update_from_dict(idx_dict) + + # populate the DB with the contents of the source file + ids_in_db = populate_db_from_file( + filename, + self.storage.db, + self.id, + self.meta_index, + utterance_start_index, + utterance_end_index, + exclude_utterance_meta, + exclude_conversation_meta, + exclude_speaker_meta, + exclude_overall_meta, + ) + + # with the StorageManager's DB now populated, initialize the corresponding + # CorpusComponent instances. + init_corpus_from_storage_manager(self, ids_in_db) + + self.meta_index.enable_type_check() + # load preload_vectors + if preload_vectors is not None: + for vector_name in preload_vectors: + matrix = ConvoKitMatrix.from_dir(self.corpus_dirpath, vector_name) + if matrix is not None: + self._vector_matrices[vector_name] = matrix + + if merge_lines: + self.utterances = merge_utterance_lines(self.utterances) + else: + # Construct corpus from file or directory + if filename is not None: + if disable_type_check: + self.meta_index.disable_type_check() + if os.path.isdir(filename): + utterances = load_utterance_info_from_dir( + filename, utterance_start_index, utterance_end_index, exclude_utterance_meta + ) + + speakers_data = load_speakers_data_from_dir(filename, exclude_speaker_meta) + convos_data = load_convos_data_from_dir(filename, exclude_conversation_meta) + load_corpus_meta_from_dir(filename, self.meta, exclude_overall_meta) + + with open(os.path.join(filename, "index.json"), "r") as f: + idx_dict = json.load(f) + self.meta_index.update_from_dict(idx_dict) + + # unpack all binary data + unpack_all_binary_data( + filename=filename, + meta_index=self.meta_index, + meta=self.meta, + utterances=utterances, + speakers_data=speakers_data, + convos_data=convos_data, + exclude_utterance_meta=exclude_utterance_meta, + exclude_speaker_meta=exclude_speaker_meta, + exclude_conversation_meta=exclude_conversation_meta, + exclude_overall_meta=exclude_overall_meta, + ) + + else: + speakers_data = defaultdict(dict) + convos_data = defaultdict(dict) + utterances = load_from_utterance_file( + filename, utterance_start_index, utterance_end_index + ) + + self.utterances = dict() + self.speakers = dict() + + initialize_speakers_and_utterances_objects(self, utterances, speakers_data) + + self.meta_index.enable_type_check() + + # load preload_vectors + if preload_vectors is not None: + for vector_name in preload_vectors: + matrix = ConvoKitMatrix.from_dir(self.corpus_dirpath, vector_name) + if matrix is not None: + self._vector_matrices[vector_name] = matrix + + elif utterances is not None: # Construct corpus from utterances list + self.speakers = {utt.speaker.id: utt.speaker for utt in utterances} + self.utterances = {utt.id: utt for utt in utterances} + for speaker in self.speakers.values(): + speaker.owner = self + for utt in self.utterances.values(): + utt.owner = self + + if merge_lines: + self.utterances = merge_utterance_lines(self.utterances) + + if disable_type_check: + self.meta_index.disable_type_check() + # if corpus is nonempty (check for self.utterances), construct the conversation + # data from the utterance list + if hasattr(self, "utterances"): + self.conversations = initialize_conversations( + self, convos_data, fill_missing_convo_ids=True + ) + self.meta_index.enable_type_check() + self.update_speakers_data() + + def update_speakers_data(self) -> None: + """ + Updates the conversation and utterance lists of every Speaker in the Corpus + :return: None + """ + speakers_utts = defaultdict(list) + speakers_convos = defaultdict(list) + + for utt in self.iter_utterances(): + speakers_utts[utt.speaker.id].append(utt) + + for convo in self.iter_conversations(): + for utt in convo.iter_utterances(): + speakers_convos[utt.speaker.id].append(convo) + + for speaker in self.iter_speakers(): + speaker.utterances = {utt.id: utt for utt in speakers_utts[speaker.id]} + speaker.conversations = {convo.id: convo for convo in speakers_convos[speaker.id]} + + def iter_utterances( + self, selector: Optional[Callable[[Utterance], bool]] = lambda utt: True + ) -> Generator[Utterance, None, None]: + """ + Get utterances in the Corpus, with an optional selector that filters for Utterances that should be included. + :param selector: a (lambda) function that takes an Utterance and returns True or False (i.e. include / exclude). + By default, the selector includes all Utterances in the Corpus. + :return: a generator of Utterances + """ + for v in self.utterances.values(): + if selector(v): + yield v + + def iter_conversations( + self, selector: Optional[Callable[[Conversation], bool]] = lambda convo: True + ) -> Generator[Conversation, None, None]: + """ + Get conversations in the Corpus, with an optional selector that filters for Conversations that should be included + :param selector: a (lambda) function that takes a Conversation and returns True or False (i.e. include / exclude). + By default, the selector includes all Conversations in the Corpus. + :return: a generator of Conversations + """ + for v in self.conversations.values(): + if selector(v): + yield v + + def get_utterance(self, utt_id: str) -> Utterance: + """ + Gets Utterance of the specified id from the corpus + :param utt_id: id of Utterance + :return: Utterance + """ + return self.utterances[utt_id] + + def get_conversation(self, convo_id: str) -> Conversation: + """ + Gets Conversation of the specified id from the corpus + :param convo_id: id of Conversation + :return: Conversation + """ + return self.conversations[convo_id] + + def get_speaker(self, speaker_id: str) -> Speaker: + """ + Gets Speaker of the specified id from the corpus + :param speaker_id: id of Speaker + :return: Speaker + """ + return self.speakers[speaker_id] + + def get_object(self, obj_type: str, oid: str): + """ + General Corpus object getter. Gets Speaker / Utterance / Conversation of specified id from the Corpus + :param obj_type: "speaker", "utterance", or "conversation" + :param oid: object id + :return: Corpus object of specified object type with specified object id + """ + assert obj_type in ["speaker", "utterance", "conversation"] + if obj_type == "speaker": + return self.get_speaker(oid) + elif obj_type == "utterance": + return self.get_utterance(oid) + else: + return self.get_conversation(oid) + + def iter_speakers( + self, selector: Optional[Callable[[Speaker], bool]] = lambda speaker: True + ) -> Generator[Speaker, None, None]: + """ + Get Speakers in the Corpus, with an optional selector that filters for Speakers that should be included + :param selector: a (lambda) function that takes a Speaker and returns True or False (i.e. include / exclude). + By default, the selector includes all Speakers in the Corpus. + :return: a generator of Speakers + """ + + for speaker in self.speakers.values(): + if selector(speaker): + yield speaker + + @property + def vectors(self): + return self.meta_index.vectors + + @vectors.setter + def vectors(self, new_vectors): + if not isinstance(new_vectors, type(["stringlist"])): + raise ValueError( + "The preload_vectors being set should be a list of strings, " + "where each string is the name of a vector matrix." + ) + self.meta_index.vectors = new_vectors + + def dump( + self, + name: str, + base_path: Optional[str] = None, + exclude_vectors: List[str] = None, + force_version: int = None, + overwrite_existing_corpus: bool = False, + fields_to_skip=None, + ) -> None: + """ + Dumps the corpus and its metadata to disk. Optionally, set `force_version` to a desired integer version number, + otherwise the version number is automatically incremented. + :param name: name of corpus + :param base_path: base directory to save corpus in (None to save to a default directory) + :param exclude_vectors: list of names of vector matrices to exclude from the dumping step. By default; all + vector matrices that belong to the Corpus (whether loaded or not) are dumped. + :param force_version: version number to set for the dumped corpus + :param overwrite_existing_corpus: if True, save to the path you loaded the corpus from, overriding the original corpus. + :param fields_to_skip: a dictionary of {object type: list of metadata attributes to omit when writing to disk}. object types can be one of "speaker", "utterance", "conversation", "corpus". + """ + if fields_to_skip is None: + fields_to_skip = dict() + dir_name = name + if base_path is not None and overwrite_existing_corpus: + raise ValueError("Not allowed to specify both base_path and overwrite_existing_corpus!") + if overwrite_existing_corpus and self.corpus_dirpath is None: + raise ValueError( + "Cannot use save to existing path on Corpus generated from utterance list!" + ) + if not overwrite_existing_corpus: + if base_path is None: + base_path = os.path.expanduser("~/.convokit/") + if not os.path.exists(base_path): + os.mkdir(base_path) + base_path = os.path.join(base_path, "saved-corpora/") + if not os.path.exists(base_path): + os.mkdir(base_path) + dir_name = os.path.join(base_path, dir_name) + else: + dir_name = os.path.join(self.corpus_dirpath) + + if not os.path.exists(dir_name): + os.mkdir(dir_name) + + # dump speakers, conversations, utterances + dump_corpus_component( + self, dir_name, "speakers.json", "speaker", "speaker", exclude_vectors, fields_to_skip + ) + dump_corpus_component( + self, + dir_name, + "conversations.json", + "conversation", + "convo", + exclude_vectors, + fields_to_skip, + ) + dump_utterances(self, dir_name, exclude_vectors, fields_to_skip) + + # dump corpus + with open(os.path.join(dir_name, "corpus.json"), "w") as f: + d_bin = defaultdict(list) + meta_up = dump_helper_bin(self.meta, d_bin, fields_to_skip.get("corpus", None)) + + json.dump(meta_up, f) + for name, l_bin in d_bin.items(): + with open(os.path.join(dir_name, name + "-overall-bin.p"), "wb") as f_pk: + pickle.dump(l_bin, f_pk) + + # dump index + with open(os.path.join(dir_name, "index.json"), "w") as f: + json.dump( + self.meta_index.to_dict( + exclude_vectors=exclude_vectors, force_version=force_version + ), + f, + ) + + # dump vectors + if exclude_vectors is not None: + vectors_to_dump = [v for v in self.vectors if v not in set(exclude_vectors)] + else: + vectors_to_dump = self.vectors + for vector_name in vectors_to_dump: + if vector_name in self._vector_matrices: + self._vector_matrices[vector_name].dump(dir_name) + else: + src = os.path.join(self.corpus_dirpath, "vectors.{}.p".format(vector_name)) + dest = os.path.join(dir_name, "vectors.{}.p".format(vector_name)) + shutil.copy(src, dest) + + # with open(os.path.join(dir_name, "processed_text.index.json"), "w") as f: + # json.dump(list(self.processed_text.keys()), f) + + def get_object_ids( + self, + obj_type: str, + selector: Callable[[Union[Speaker, Utterance, Conversation]], bool] = lambda obj: True, + ): + """ + Get a list of ids of Corpus objects of the specified type in the Corpus, with an optional selector that filters for objects that should be included + :param obj_type: "speaker", "utterance", or "conversation" + :param selector: a (lambda) function that takes a Corpus object and returns True or False (i.e. include / exclude). + By default, the selector includes all objects of the specified type in the Corpus. + :return: list of Corpus object ids + """ + assert obj_type in ["speaker", "utterance", "conversation"] + return [obj.id for obj in self.iter_objs(obj_type, selector)] + + def iter_objs( + self, + obj_type: str, + selector: Callable[[Union[Speaker, Utterance, Conversation]], bool] = lambda obj: True, + ): + """ + Get Corpus objects of specified type from the Corpus, with an optional selector that filters for Corpus object that should be included + :param obj_type: "speaker", "utterance", or "conversation" + :param selector: a (lambda) function that takes a Corpus object and returns True or False (i.e. include / exclude). + By default, the selector includes all objects of the specified type in the Corpus. + :return: a generator of Speakers + """ + + assert obj_type in ["speaker", "utterance", "conversation"] + obj_iters = { + "conversation": self.iter_conversations, + "speaker": self.iter_speakers, + "utterance": self.iter_utterances, + } + + return obj_iters[obj_type](selector) + + @staticmethod + def from_pandas( + utterances_df: DataFrame, + speakers_df: Optional[DataFrame] = None, + conversations_df: Optional[DataFrame] = None, + ) -> "Corpus": + """ + Generates a Corpus from utterances, speakers, and conversations dataframes. + For each dataframe, if the 'id' column is absent, the dataframe index will be used as the id. + Metadata should be denoted with a 'meta.' column in the dataframe. For example, if an utterance is to have + a metadata key 'score', then the 'meta.score' column must be present in dataframe. + `speakers_df` and `conversations_df` are optional, as their IDs can be inferred from `utterances_df`, and so + their main purpose is to hold speaker / conversation metadata. They should only be included if there exists + metadata for the speakers / conversations respectively. + Metadata values that are not basic Python data structures (i.e. lists, dicts, tuples) may be included in the + dataframes but may lead to unexpected behavior, depending on how `pandas` serializes / deserializes those values. + Note that as metadata can be added to the Corpus after it is constructed, there is no need to include all + metadata keys in the dataframe if it would be inconvenient. + :param utterances_df: utterances data in a pandas Dataframe, all primary data fields expected, with metadata optional + :param speakers_df: (optional) speakers data in a pandas Dataframe + :param conversations_df: (optional) conversations data in a pandas Dataframe + :return: Corpus constructed from the dataframe(s) + """ + columns = ["speaker", "id", "timestamp", "conversation_id", "reply_to", "text"] + + for df_type, df in [ + ("utterances", utterances_df), + ("conversations", conversations_df), + ("speakers", speakers_df), + ]: + if df is None: + continue + if "id" not in df.columns: + print( + f"ID column is not present in {df_type} dataframe, generated ID column from dataframe index..." + ) + df["id"] = df.index + + # checking if dataframes contain their respective required columns + assert ( + pd.Series(columns).isin(utterances_df.columns).all() + ), "Utterances dataframe must contain all primary data fields" + + utterance_meta_cols = extract_meta_from_df(utterances_df) + + utterance_list = [] + for index, row in tqdm(utterances_df.iterrows()): + if utterance_meta_cols: + metadata = {} + for meta_col in utterance_meta_cols: + metadata[meta_col] = row["meta." + meta_col] + else: + metadata = None + + # adding utterance in utterance list + reply_to = None if row["reply_to"] == "None" else row["reply_to"] + utterance_list.append( + Utterance( + id=str(row["id"]), + speaker=Speaker(id=str(row["speaker"])), + conversation_id=str(row["conversation_id"]), + reply_to=reply_to, + timestamp=row["timestamp"], + text=row["text"], + meta=metadata, + ) + ) + + # initializing corpus using utterance_list + corpus = Corpus(utterances=utterance_list) + if speakers_df is not None: + corpus.update_metadata_from_df("speaker", speakers_df) + if conversations_df is not None: + corpus.update_metadata_from_df("conversation", conversations_df) + + return corpus + +class MemStorageManager(StorageManager): + """ + Concrete StorageManager implementation for in-memory data storage. + Collections are implemented as vanilla Python dicts. + """ + + def __init__(self): + super().__init__() + + # initialize component collections as dicts + for key in self.data: + self.data[key] = {} + + def get_collection_ids(self, component_type: str): + return list(self.get_collection(component_type).keys()) + + def has_data_for_component(self, component_type: str, component_id: str) -> bool: + collection = self.get_collection(component_type) + return component_id in collection + + def initialize_data_for_component( + self, component_type: str, component_id: str, overwrite: bool = False, initial_value=None + ): + collection = self.get_collection(component_type) + if overwrite or not self.has_data_for_component(component_type, component_id): + collection[component_id] = initial_value if initial_value is not None else {} + + def get_data( + self, + component_type: str, + component_id: str, + property_name: Optional[str] = None, + index=None, + ): + collection = self.get_collection(component_type) + if component_id not in collection: + raise KeyError( + f"This StorageManager does not have an entry for the {component_type} with id {component_id}." + ) + if property_name is None: + return collection[component_id] + else: + return collection[component_id][property_name] + + def update_data( + self, + component_type: str, + component_id: str, + property_name: str, + new_value, + index=None, + ): + collection = self.get_collection(component_type) + # don't create new collections if the ID is not found; this is supposed to be handled in the + # CorpusComponent constructor so if the ID is missing that indicates something is wrong + if component_id not in collection: + raise KeyError( + f"This StorageManager does not have an entry for the {component_type} with id {component_id}." + ) + collection[component_id][property_name] = new_value + + def delete_data( + self, component_type: str, component_id: str, property_name: Optional[str] = None + ): + collection = self.get_collection(component_type) + if component_id not in collection: + raise KeyError( + f"This StorageManager does not have an entry for the {component_type} with id {component_id}." + ) + if property_name is None: + del collection[component_id] + else: + del collection[component_id][property_name] + + def clear_all_data(self): + for key in self.data: + self.data[key] = {} + + def count_entries(self, component_type: str): + return len(self.get_collection(component_type)) + + +class DBStorageManager(StorageManager): + """ + Concrete StorageManager implementation for database-backed data storage. + Collections are implemented as MongoDB collections. + """ + + def __init__(self, collection_prefix, db_host: Optional[str] = None): + super().__init__() + + self.collection_prefix = collection_prefix + self.client = MongoClient(db_host) + self.db = self.client["convokit"] + + # this special lock is used for reconnecting to an existing DB, whereupon + # it is known that all the data already exists and so the initialization + # step can be skipped, greatly saving time + self.bypass_init = False + + # initialize component collections as MongoDB collections in the convokit db + for key in self.data: + self.data[key] = self.db[self._get_collection_name(key)] + + def _get_collection_name(self, component_type: str) -> str: + return f"{self.collection_prefix}_{component_type}" + + def get_collection_ids(self, component_type: str): + return [ + doc["_id"] + for doc in self.db[self._get_collection_name(component_type)].find(projection=["_id"]) + ] + + def has_data_for_component(self, component_type: str, component_id: str) -> bool: + collection = self.get_collection(component_type) + lookup = collection.find_one({"_id": component_id}) + return lookup is not None + + def initialize_data_for_component( + self, component_type: str, component_id: str, overwrite: bool = False, initial_value=None + ): + if self.bypass_init: + return + collection = self.get_collection(component_type) + if overwrite or not self.has_data_for_component(component_type, component_id): + data = initial_value if initial_value is not None else {} + collection.replace_one({"_id": component_id}, data, upsert=True) + + def get_data( + self, + component_type: str, + component_id: str, + property_name: Optional[str] = None, + index=None, + ): + collection = self.get_collection(component_type) + all_fields = collection.find_one({"_id": component_id}) + if all_fields is None: + raise KeyError( + f"This StorageManager does not have an entry for the {component_type} with id {component_id}." + ) + if property_name is None: + # if some data is known to be binary type, unpack it + if index is not None: + for key in all_fields: + if index.get(key, None) == ["bin"]: + all_fields[key] = pickle.loads(all_fields[key]) + # do not include the MongoDB-specific _id field + del all_fields["_id"] + return all_fields + else: + result = all_fields[property_name] + if index is not None and index.get(property_name, None) == ["bin"]: + # binary data must be unpacked + result = pickle.loads(result) + return result + + def update_data( + self, + component_type: str, + component_id: str, + property_name: str, + new_value, + index=None, + ): + data = self.get_data(component_type, component_id) + if index is not None and index.get(property_name, None) == ["bin"]: + # non-serializable types must go through pickling then be encoded as bson.Binary + new_value = bson.Binary(pickle.dumps(new_value)) + data[property_name] = new_value + collection = self.get_collection(component_type) + collection.update_one({"_id": component_id}, {"$set": data}) + + def delete_data( + self, component_type: str, component_id: str, property_name: Optional[str] = None + ): + collection = self.get_collection(component_type) + if property_name is None: + # delete the entire document + collection.delete_one({"_id": component_id}) + else: + # delete only the specified property + collection.update_one({"_id": component_id}, {"$unset": {property_name: ""}}) + + def clear_all_data(self): + for key in self.data: + self.data[key].drop() + self.data[key] = self.db[self._get_collection_name(key)] + + def count_entries(self, component_type: str): + return self.get_collection(component_type).estimated_document_count() + +class ConvoKitIndex: + def __init__( + self, + owner, + utterances_index: Optional[Dict[str, List[str]]] = None, + speakers_index: Optional[Dict[str, List[str]]] = None, + conversations_index: Optional[Dict[str, List[str]]] = None, + overall_index: Optional[Dict[str, List[str]]] = None, + vectors: Optional[List[str]] = None, + version: Optional[int] = 0, + ): + self.owner = owner + self.utterances_index = utterances_index if utterances_index is not None else {} + self.speakers_index = speakers_index if speakers_index is not None else {} + self.conversations_index = conversations_index if conversations_index is not None else {} + self.overall_index = overall_index if overall_index is not None else {} + self.indices = { + "utterance": self.utterances_index, + "conversation": self.conversations_index, + "speaker": self.speakers_index, + "corpus": self.overall_index, + } + self.vectors = set(vectors) if vectors is not None else set() + self.version = version + self.type_check = True # toggle-able to enable/disable type checks on metadata additions + self.lock_metadata_deletion = {"utterance": True, "conversation": True, "speaker": True} + + def create_new_index(self, obj_type: str, key: str): + """ + Create a new entry in the obj_type index with a blank type list, + representing an "Any" type which might be later refined. + :param obj_type: utterance, conversation, or speaker + :param key: string + :param class_type: class type + """ + if key not in self.indices[obj_type]: + self.indices[obj_type][key] = [] + + def update_index(self, obj_type: str, key: str, class_type: str): + """ + Append the class_type to the index + :param obj_type: utterance, conversation, or speaker + :param key: string + :param class_type: class type + :return: None + """ + assert type(key) == str + assert "class" in class_type or class_type == "bin" + if key not in self.indices[obj_type]: + self.indices[obj_type][key] = [] + self.indices[obj_type][key].append(class_type) + + def set_index(self, obj_type: str, key: str, class_type: str): + """ + Set the class_type of the index as [`class_type`]. + :param obj_type: utterance, conversation, or speaker + :param key: string + :param class_type: class type + :return: None + """ + assert type(key) == str + assert "class" in class_type or class_type == "bin" + self.indices[obj_type][key] = [class_type] + + def get_index(self, obj_type: str): + return self.indices[obj_type] + + def del_from_index(self, obj_type: str, key: str): + assert type(key) == str + if key not in self.indices[obj_type]: + return + del self.indices[obj_type][key] + # + # corpus = self.owner + # for corpus_obj in corpus.iter_objs(obj_type): + # if key in corpus_obj.meta: + # del corpus_obj.meta[key] + + def add_vector(self, vector_name): + self.vectors.add(vector_name) + + def del_vector(self, vector_name): + self.vectors.remove(vector_name) + + def update_from_dict(self, meta_index: Dict): + self.conversations_index.update(meta_index["conversations-index"]) + self.utterances_index.update(meta_index["utterances-index"]) + speaker_index = "speakers-index" if "speakers-index" in meta_index else "users-index" + self.speakers_index.update(meta_index[speaker_index]) + self.overall_index.update(meta_index["overall-index"]) + self.vectors = set(meta_index.get("vectors", set())) + for index in self.indices.values(): + for k, v in index.items(): + if isinstance(v, str): + index[k] = [v] + + self.version = meta_index["version"] + + def to_dict(self, exclude_vectors: List[str] = None, force_version=None): + retval = dict() + retval["utterances-index"] = self.utterances_index + retval["speakers-index"] = self.speakers_index + retval["conversations-index"] = self.conversations_index + retval["overall-index"] = self.overall_index + + if force_version is None: + retval["version"] = self.version + 1 + else: + retval["version"] = force_version + + if exclude_vectors is not None: + retval["vectors"] = list(self.vectors - set(exclude_vectors)) + else: + retval["vectors"] = list(self.vectors) + + return retval + + def enable_type_check(self): + self.type_check = True + + def disable_type_check(self): + self.type_check = False + + def __str__(self): + return str(self.to_dict(force_version=self.version)) + + def __repr__(self): + return str(self) + + +### HELPER FUNCTIONS + +def extract_meta_from_df(df): + meta_cols = [col.split(".")[1] for col in df if col.startswith("meta")] + return meta_cols + +def get_corpus_dirpath(filename: str) -> Optional[str]: + if filename is None: + return None + elif os.path.isdir(filename): + return filename + else: + return os.path.dirname(filename) + +def get_corpus_id( + db_collection_prefix: Optional[str], filename: Optional[str], storage_type: str +) -> Optional[str]: + if db_collection_prefix is not None: + # treat the unique collection prefix as the ID (even if a filename is specified) + corpus_id = db_collection_prefix + elif filename is not None: + # automatically derive an ID from the file path + corpus_id = os.path.basename(os.path.normpath(filename)) + else: + corpus_id = None + + if storage_type == "db" and corpus_id is not None: + compatibility_msg = check_id_for_mongodb(corpus_id) + if compatibility_msg is not None: + random_id = create_safe_id() + warn( + f'Attempting to use "{corpus_id}" as DB collection prefix failed because: {compatibility_msg}. Will instead use randomly generated prefix {random_id}.' + ) + corpus_id = random_id + + return corpus_id + +def initialize_storage( + corpus: "Corpus", storage: Optional[StorageManager], storage_type: str, db_host: Optional[str] +): + if storage is not None: + return storage + else: + if storage_type == "mem": + return MemStorageManager() + elif storage_type == "db": + if db_host is None: + db_host = corpus.config.db_host + return DBStorageManager(corpus.id, db_host) + else: + raise ValueError( + f"Unrecognized setting '{storage_type}' for storage type; should be either 'mem' or 'db'." + ) + +def initialize_conversations( + corpus, convos_data, convo_to_utts=None, fill_missing_convo_ids: bool = False +): + """ + Initialize Conversation objects from utterances and conversations data. + If a mapping from Conversation IDs to their constituent Utterance IDs is + already known (e.g., as a side effect of a prior computation) they can be + directly provided via the convo_to_utts parameter, otherwise the mapping + will be computed by iteration over the Utterances in utt_dict. + """ + if fill_missing_convo_ids: + fill_missing_conversation_ids(corpus.utterances) + + # organize utterances by conversation + if convo_to_utts is None: + convo_to_utts = defaultdict(list) # temp container identifying utterances by conversation + for utt in corpus.utterances.values(): + convo_key = ( + utt.conversation_id + ) # each conversation_id is considered a separate conversation + convo_to_utts[convo_key].append(utt.id) + conversations = {} + for convo_id in convo_to_utts: + # look up the metadata associated with this conversation, if any + convo_data = convos_data.get(convo_id, None) + if convo_data is not None: + if KeyMeta in convo_data: + convo_meta = convo_data[KeyMeta] + else: + convo_meta = convo_data + else: + convo_meta = None + + convo = Conversation( + owner=corpus, id=convo_id, utterances=convo_to_utts[convo_id], meta=convo_meta + ) + + if convo_data is not None and KeyVectors in convo_data and KeyMeta in convo_data: + convo.vectors = convo_data.get(KeyVectors, []) + conversations[convo_id] = convo + return conversations + + +def fill_missing_conversation_ids(utterances_dict: Dict[str, Utterance]) -> None: + """ + Populates `conversation_id` in Utterances that have `conversation_id` set to `None`, with a Conversation root-specific generated ID + :param utterances_dict: + :return: + """ + utts_without_convo_ids = [ + utt for utt in utterances_dict.values() if utt.conversation_id is None + ] + utt_ids_to_replier_ids = defaultdict(deque) + convo_roots_without_convo_ids = [] + convo_roots_with_convo_ids = [] + for utt in utterances_dict.values(): + if utt.reply_to is None: + if utt.conversation_id is None: + convo_roots_without_convo_ids.append(utt.id) + else: + convo_roots_with_convo_ids.append(utt.id) + else: + utt_ids_to_replier_ids[utt.reply_to].append(utt.id) + + # connect the reply-to edges for convo roots without convo ids + for root_utt_id in convo_roots_without_convo_ids: + generated_conversation_id = Conversation.generate_default_conversation_id( + utterance_id=root_utt_id + ) + utterances_dict[root_utt_id].conversation_id = generated_conversation_id + _update_reply_to_chain_with_conversation_id( + utterances_dict=utterances_dict, + utt_ids_to_replier_ids=utt_ids_to_replier_ids, + root_utt_id=root_utt_id, + conversation_id=generated_conversation_id, + ) + + # Previous section handles all *new* conversations + # Next section handles utts that belong to existing conversations + for root_utt_id in convo_roots_with_convo_ids: + conversation_id = utterances_dict[root_utt_id].conversation_id + _update_reply_to_chain_with_conversation_id( + utterances_dict=utterances_dict, + utt_ids_to_replier_ids=utt_ids_to_replier_ids, + root_utt_id=root_utt_id, + conversation_id=conversation_id, + ) + + # It's still possible to have utts that reply to non-existent utts + # These are the utts that do not have a conversation_id even at this step + for utt in utts_without_convo_ids: + if utt.conversation_id is None: + raise ValueError( + f"Invalid Utterance found: Utterance {utt.id} replies to an Utterance '{utt.reply_to}' that does not exist." + ) + +def _update_reply_to_chain_with_conversation_id( + utterances_dict: Dict[str, Utterance], + utt_ids_to_replier_ids: Dict[str, Iterable[str]], + root_utt_id: str, + conversation_id: str, +): + repliers = utt_ids_to_replier_ids.get(root_utt_id, deque()) + while len(repliers) > 0: + replier_id = repliers.popleft() + utterances_dict[replier_id].conversation_id = conversation_id + repliers.extend(utt_ids_to_replier_ids[replier_id]) + +def dump_corpus_component( + corpus, dir_name, filename, obj_type, bin_name, exclude_vectors, fields_to_skip +): + with open(os.path.join(dir_name, filename), "w") as f: + d_bin = defaultdict(list) + objs = defaultdict(dict) + for obj_id in corpus.get_object_ids(obj_type): + objs[obj_id][KeyMeta] = dump_helper_bin( + corpus.get_object(obj_type, obj_id).meta, d_bin, fields_to_skip.get(obj_type, []) + ) + obj_vectors = corpus.get_object(obj_type, obj_id).vectors + objs[obj_id][KeyVectors] = ( + obj_vectors + if exclude_vectors is None + else list(set(obj_vectors) - set(exclude_vectors)) + ) + json.dump(objs, f) + + for name, l_bin in d_bin.items(): + with open(os.path.join(dir_name, name + "-{}-bin.p".format(bin_name)), "wb") as f_pk: + pickle.dump(l_bin, f_pk) + +BIN_DELIM_L, BIN_DELIM_R = "<##bin{", "}&&@**>" +KeyId = "id" +KeySpeaker = "speaker" +KeyConvoId = "conversation_id" +KeyReplyTo = "reply-to" +KeyTimestamp = "timestamp" +KeyText = "text" +DefinedKeys = {KeyId, KeySpeaker, KeyConvoId, KeyReplyTo, KeyTimestamp, KeyText} +KeyMeta = "meta" +KeyVectors = "vectors" + +JSONLIST_BUFFER_SIZE = 1000 + +def dump_helper_bin(d: ConvoKitMeta, d_bin: Dict, fields_to_skip=None) -> Dict: # object_idx + """ + :param d: The ConvoKitMeta to encode + :param d_bin: The dict of accumulated lists of binary attribs + :return: + """ + if fields_to_skip is None: + fields_to_skip = [] + + obj_idx = d.index.get_index(d.obj_type) + d_out = {} + for k, v in d.items(): + if k in fields_to_skip: + continue + try: + if len(obj_idx[k]) > 0 and obj_idx[k][0] == "bin": + d_out[k] = "{}{}{}".format(BIN_DELIM_L, len(d_bin[k]), BIN_DELIM_R) + d_bin[k].append(v) + else: + d_out[k] = v + except KeyError: + # fails silently (object has no such metadata that was indicated in metadata index) + pass + return d_out + +def dump_utterances(corpus, dir_name, exclude_vectors, fields_to_skip): + with open(os.path.join(dir_name, "utterances.jsonl"), "w") as f: + d_bin = defaultdict(list) + + for ut in corpus.iter_utterances(): + ut_obj = { + KeyId: ut.id, + KeyConvoId: ut.conversation_id, + KeyText: ut.text, + KeySpeaker: ut.speaker.id, + KeyMeta: dump_helper_bin(ut.meta, d_bin, fields_to_skip.get("utterance", [])), + KeyReplyTo: ut.reply_to, + KeyTimestamp: ut.timestamp, + KeyVectors: ut.vectors + if exclude_vectors is None + else list(set(ut.vectors) - set(exclude_vectors)), + } + json.dump(ut_obj, f) + f.write("\n") + + for name, l_bin in d_bin.items(): + with open(os.path.join(dir_name, name + "-bin.p"), "wb") as f_pk: + pickle.dump(l_bin, f_pk) diff --git a/sktalk/read_audio.py b/sktalk/read_audio.py new file mode 100644 index 0000000..b549288 --- /dev/null +++ b/sktalk/read_audio.py @@ -0,0 +1,38 @@ +import subprocess +import json +import numpy as np + + +def load_audio(file_path): + cmd = [ + "ffprobe", + "-v", "quiet", + "-print_format", "json", + "-show_streams", + file_path + ] + + result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + output = json.loads(result.stdout) + + sample_rate = None + for stream in output["streams"]: + if stream["codec_type"] == "audio": + sample_rate = stream["sample_rate"] + no_channels = stream["channels"] #TODO the channels need to be preserved + + if sample_rate is None: + raise ValueError("No audio stream found in the file") + + cmd = ["ffmpeg", "-i", file_path, '-f', 's16le', + '-acodec', 'pcm_s16le', + '-ar', sample_rate, + '-ac', '1', + '-'] + + pipe = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + raw_audio = pipe.stdout + audio_array = np.frombuffer(raw_audio, dtype="int16") + audio_array = audio_array.astype(np.float32) / np.iinfo(np.int16).max + + return audio_array, int(sample_rate) \ No newline at end of file