From 703aefe385add1778c28f3897b8f392fc12c048d Mon Sep 17 00:00:00 2001 From: Ivan Smirnov <34346831+Lameus@users.noreply.github.com> Date: Thu, 14 Dec 2023 12:59:01 +0300 Subject: [PATCH] Added new example case (#61) * add flag for results of congr, fix token for diar * Update requirements.txt * updated pdm files * added example, fixed stt and congr * added example, dropped ioynb from ignore * added case example --------- Co-authored-by: Anatolii Medvedev <83948828+mdvdv@users.noreply.github.com> --- .../call_analysis_topic_extraction.ipynb | 696 ++++++++++++++++++ examples/cases/call_stats.py | 240 ++++++ examples/cases/example_utils.py | 103 +++ expert/core/congruence/congruence_analysis.py | 10 +- 4 files changed, 1043 insertions(+), 6 deletions(-) create mode 100644 examples/cases/call_analysis_topic_extraction.ipynb create mode 100755 examples/cases/call_stats.py create mode 100644 examples/cases/example_utils.py diff --git a/examples/cases/call_analysis_topic_extraction.ipynb b/examples/cases/call_analysis_topic_extraction.ipynb new file mode 100644 index 0000000..938e98d --- /dev/null +++ b/examples/cases/call_analysis_topic_extraction.ipynb @@ -0,0 +1,696 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Audio preprocessing" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/izs/miniconda3/envs/relevant/lib/python3.9/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n", + "/home/izs/.local/lib/python3.9/site-packages/whisper/timing.py:57: NumbaDeprecationWarning: \u001b[1mThe 'nopython' keyword argument was not supplied to the 'numba.jit' decorator. The implicit default value for this argument is currently False, but it will be changed to True in Numba 0.59.0. See https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit for details.\u001b[0m\n", + " def backtrace(trace: np.ndarray):\n" + ] + } + ], + "source": [ + "import torch\n", + "\n", + "from examples.cases import example_utils\n", + "from examples.cases.call_stats import DialogStats, call_statistic\n", + "from expert.data.annotation import speech_to_text\n", + "from expert.data.diarization.speaker_diarization import SpeakerDiarization" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "file_path = \"examples/cases/test_aud.wav\"\n", + "\n", + "Diarization = SpeakerDiarization(audio=file_path, device=torch.device(\"cpu\"))\n", + "speakers = Diarization.apply()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "transcribation = speech_to_text.transcribe_video(file_path, lang=\"ru\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "all_words = speech_to_text.get_all_words(transcribation)[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "timestamps = []\n", + "for speaker in speakers:\n", + " for elem in speakers[speaker]:\n", + " timestamps.append((elem, speaker))" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "sentences = example_utils.sentences_with_time(timestamps, all_words)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Connecting sentences with speech intevals form diarization" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{0: (' Здравствуйте', ([0, 3], 'SPEAKER_01')),\n", + " 1: (' Мне нужна помощь. Я забыла, но вдруг внезапно обнаружила, что у меня есть карточка вашего банка. И на ней написано И тут я вспоминаю, что мне в паре карточку впалили. Думаю, ну ладно, пусть будет. И даже на ней моё имя выбито. Но когда я её получала? Что это за карточка? Актуально, я так поняла, до года, прямо на вообще. Моя фамилия',\n", + " ([2, 36], 'SPEAKER_00')),\n", + " 2: (' Проверьте', ([36, 37], 'SPEAKER_01')),\n", + " 3: (' и скажите, что мне надо делать с этой карточкой, какие плюшки она мне дает.',\n", + " ([37, 42], 'SPEAKER_00')),\n", + " 4: (' ДС?', ([42, 44], 'SPEAKER_01')),\n", + " 5: (' Нет, через О В секунду', ([44, 50], 'SPEAKER_00')),\n", + " 6: (' Я даже не знаю, какой кот у этой карточки. Сейчас посмотрим, скажу вам.',\n", + " ([50, 57], 'SPEAKER_01')),\n", + " 7: ('', ([51, 55], 'SPEAKER_00')),\n", + " 8: (' Обычно на карточках расписываются. У меня тут подлинные стахи.',\n", + " ([63, 68], 'SPEAKER_00')),\n", + " 9: ('', ([68, 69], 'SPEAKER_01')),\n", + " 10: (' Так, дата рождения ваша?', ([74, 76], 'SPEAKER_01')),\n", + " 11: (' Четвёртая, двенадцать, с отвощенного.', ([76, 80], 'SPEAKER_00')),\n", + " 12: (' Так, и Да, верно назвали. Так, у вас пакет',\n", + " ([84.0, 92], 'SPEAKER_01')),\n", + " 13: (' услуг нового.', ([92, 95], 'SPEAKER_00')),\n", + " 14: (' Это сомнестный продукт с Скажите, у вас оптиматор какой? Да, да. Ну, а один теперь это. Так, вот, смотрите. Вы из этого, как вы, в том числе, этот пакет можете получать полутарифу, 50% в скидке, если этот риф в акции указан. Ну, если вы в бомоформуле, я думаю, у вас в астротариф указан. У вас карта сейчас не активирована. Скитка пока что вас не действует. Что надо делать? Нужно скачать инсинг для начала мобильное приложение. Так, а теперь стоп. Теперь стоп. Мой телефон не позволяет',\n", + " ([95, 135], 'SPEAKER_01')),\n", + " 15: ('', ([107, 111], 'SPEAKER_00')),\n", + " 16: ('', ([127, 129], 'SPEAKER_00')),\n", + " 17: (' скачать это приложение. И, конечно, это не так. Я живу. Какие у меня еще варианты есть? В отделении банка. В отделении банка спрашивают, вам помогут активировать вам все. Скажите, в любой отделении или? Ну, думаю, во всех отделениях будет именно планшет, чтобы вам с их устройства помогут активировать.',\n", + " ([132, 155], 'SPEAKER_00')),\n", + " 18: ('', ([145, 146], 'SPEAKER_01')),\n", + " 19: ('', ([147, 154], 'SPEAKER_01')),\n", + " 20: (' Это так будет происходить. В каком городе находитесь? В Минске. В Минске, думаю, вообще, в Минске. В Минске, в Минске. В',\n", + " ([156, 166], 'SPEAKER_01')),\n", + " 21: ('', ([163, 164], 'SPEAKER_00')),\n", + " 22: (' Минске, думаю,', ([165, 167], 'SPEAKER_00')),\n", + " 23: (' во всех точно будет. В любое оборотение', ([167, 169], 'SPEAKER_01')),\n", + " 24: (' отделения вам активирует карту. И на следующий день уже будет действовать скидка. По поводу комиссии, этот пакет из лечебничной комиссии нет никакой. Там сказали, что какую-то сумму нужно мне положить на эту карту. Должна быть сумма. Сумма.',\n", + " ([169.68, 185], 'SPEAKER_01')),\n", + " 25: (' Сейчас по комиссии.', ([183, 187], 'SPEAKER_00')),\n", + " 26: (' Я не очень поняла, когда', ([188.48, 191], 'SPEAKER_00')),\n", + " 27: (' просто пустая карта, она будет скачать. Я',\n", + " ([191, 194], 'SPEAKER_01')),\n", + " 28: (' не очень поняла, когда просто пустая карта смысл в ее. Но вроде как, какая-то сумма должна быть или что-то там должно покупаться с этой карты.',\n", + " ([193, 202], 'SPEAKER_00')),\n", + " 29: (' Если у вас 6 месяцев будет без учета критериев. По этой карте, да, есть критерия, нужно, чтобы была сумма, и вы 500 до русских рублей в месяц должны тратить. Но это на выполнение этого критерия не требуется. Если вы активируете карту до 31 декабря, и у вас этот период будет 6 месяцев. То есть, у вас будет эти 6 месяцев на 60-60 китках 50%. И тогда критериев выполнять не нужно. Также, если вы будете в соседях либо в магазине грин рассчитываетесь. Бывает.',\n", + " ([202, 232], 'SPEAKER_01')),\n", + " 30: (' За расчет', ([233, 235], 'SPEAKER_00')),\n", + " 31: (' в этих магазинах вы будете получать бонусы. То, будешь, просто купили продукты. У вас есть бонусы. Вы сможете, накопив бонусы, купить, к примеру, какой-то талон в магазине грин, на 30 рублей. Вы тратите, накапливаете баллы, и потом сможете купить. Вот такой купон.',\n", + " ([235, 259], 'SPEAKER_01')),\n", + " 32: (' Ради того, чтобы платить не 30, а 15 рублей, нужно в месяц тратить 500 рублей. Такая замануха, ну, прямо смешно даже иногда. По логике, да? То есть, я соглашаю. Ну, и то меня я пришла выяснить один вопрос. А один. Давайте, давайте. Я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Ну, если... Ну, если... Ну, вы согласитесь по человеку. Если вы отойдете от всех ваших дел банковских, просто по-человечески, принцип такой, чтобы тратить вам 15, а не 30, пожалуйста, тратите с карточки 500. Ну, если тратить именно... Я туда же до них положу еще.',\n", + " ([257, 325], 'SPEAKER_00')),\n", + " 33: ('', ([268, 270], 'SPEAKER_01')),\n", + " 34: ('', ([306, 311], 'SPEAKER_01')),\n", + " 35: (' Да, значит, чтобы перерываю. Если вы именно... Я с вами не совсем согласен, потому что если вы именно так рассматриваете отдельно, тогда, да, в вашем случае вы будете правы. Но если рассматривать именно то, что у вас карта, в принципе, в белорусских рублях без комиссии, в любом случае, не все банки предоставляют вообще карты бесплатной белоруски. Это да, тут я согласна, даже вопросов нет. Бесплатная белорусская карта, она обслуживается без комиссии, бесконтактной, и дополнительно еще, как просто в Добарок, вы можете сами расплачиваться ей. И, помимо этого, вам еще и начисляется скидка 50%, и 6 месяцев это будет без комиссии. То, что вы просто оформили карту, и 6 месяцев вам не нужно тратить эти 500 белорусских рублях. Вы просто пользуетесь картой, как обычно. Ну, а потом,',\n", + " ([323, 371], 'SPEAKER_01')),\n", + " 36: ('', ([343, 347], 'SPEAKER_00')),\n", + " 37: (' если я 500 рублей не плачу, уже не получаю 50%? Если',\n", + " ([370, 376], 'SPEAKER_00')),\n", + " 38: (' 500 не тратите, то комиссия, именно скидку не получите, но пакет также без комиссии будет обслуживаться. Ну,',\n", + " ([376, 384], 'SPEAKER_01')),\n", + " 39: (' скидки то уже нет. Ну, так я с вами говорю. Пока это есть, да, какие-то плюшки я выигрываю, но потом же все равно приходит к этому принципу. В общем, из того, что вы мне сказать, в любом банке они мне помогут ее активировать. И все.',\n", + " ([383, 401], 'SPEAKER_00')),\n", + " 40: ('', ([385, 386], 'SPEAKER_01')),\n", + " 41: ('', ([400, 402], 'SPEAKER_01')),\n", + " 42: (' А дальше я там уже выбираю. Класси туда деньги, пользуйся тенизниками. Ясно, спасибо большое, Хорошего вам дня. И с первым днем зимы. У вас тоже с первым. У вас нету скидки. Спасибо. До свидания.',\n", + " ([402, 414], 'SPEAKER_00')),\n", + " 43: ('', ([409, 410], 'SPEAKER_01')),\n", + " 44: ('', ([411, 415], 'SPEAKER_01'))}" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ci = example_utils.place_words(timestamps, sentences[1])\n", + "\n", + "for num, i in enumerate(ci):\n", + " timestamps[i[1]][0][0] = min(timestamps[i[1]][0][0], sentences[1][num][1])\n", + "\n", + "timestamps.sort()\n", + "\n", + "sentences = example_utils.sentences_with_time(timestamps, all_words)\n", + "\n", + "sentences[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Extracting of specific statistics" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "from examples.cases.call_stats import DialogStats, call_statistic" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Перебивал SPEAKER_00\n", + "00:02 - 00:03\n", + "Перебивал SPEAKER_01\n", + "00:36 - 00:36\n", + "Перебивал SPEAKER_00\n", + "00:37 - 00:37\n", + "Перебивал SPEAKER_01\n", + "00:42 - 00:42\n", + "Перебивал SPEAKER_00\n", + "00:44 - 00:44\n", + "Перебивал SPEAKER_01\n", + "00:50 - 00:50\n", + "Перебивал SPEAKER_00\n", + "00:51 - 00:55\n", + "Перебивал SPEAKER_01\n", + "01:08 - 01:08\n", + "Перебивал SPEAKER_00\n", + "01:16 - 01:16\n", + "Перебивал SPEAKER_00\n", + "01:32 - 01:32\n", + "Перебивал SPEAKER_01\n", + "01:35 - 01:35\n", + "Перебивал SPEAKER_00\n", + "01:47 - 01:51\n", + "Перебивал SPEAKER_00\n", + "02:07 - 02:09\n", + "Перебивал SPEAKER_00\n", + "02:12 - 02:15\n", + "Перебивал SPEAKER_01\n", + "02:25 - 02:26\n", + "Перебивал SPEAKER_01\n", + "02:27 - 02:34\n", + "Перебивал SPEAKER_00\n", + "02:43 - 02:44\n", + "Перебивал SPEAKER_00\n", + "02:45 - 02:46\n", + "Перебивал SPEAKER_01\n", + "02:47 - 02:47\n", + "Перебивал SPEAKER_00\n", + "03:03 - 03:05\n", + "Перебивал SPEAKER_01\n", + "03:11 - 03:11\n", + "Перебивал SPEAKER_00\n", + "03:13 - 03:14\n", + "Перебивал SPEAKER_01\n", + "03:22 - 03:22\n", + "Перебивал SPEAKER_01\n", + "03:55 - 03:55\n", + "Перебивал SPEAKER_00\n", + "04:17 - 04:19\n", + "Перебивал SPEAKER_01\n", + "04:28 - 04:30\n", + "Перебивал SPEAKER_01\n", + "05:06 - 05:11\n", + "Перебивал SPEAKER_01\n", + "05:23 - 05:25\n", + "Перебивал SPEAKER_00\n", + "05:43 - 05:47\n", + "Перебивал SPEAKER_00\n", + "06:10 - 06:11\n", + "Перебивал SPEAKER_01\n", + "06:16 - 06:16\n", + "Перебивал SPEAKER_00\n", + "06:23 - 06:24\n", + "Перебивал SPEAKER_01\n", + "06:25 - 06:26\n", + "Перебивал SPEAKER_01\n", + "06:40 - 06:41\n", + "Перебивал SPEAKER_00\n", + "06:42 - 06:42\n", + "Перебивал SPEAKER_01\n", + "06:49 - 06:50\n", + "Перебивал SPEAKER_01\n", + "06:51 - 06:54\n" + ] + } + ], + "source": [ + "report = call_statistic(Diarization.audio, Diarization.sr, sentences[0], speakers)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "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", + "
Продолжительность разговора секТишина в начале секТишина в конце секДлителньость тишины секДоля тишиныПродолжительность речи SPEAKER_00Скорость речи SPEAKER_00Количество интервалов SPEAKER_00Доля речи SPEAKER_00Продолжительность речи SPEAKER_01Скорость речи SPEAKER_01Количество интервалов SPEAKER_01Доля речи SPEAKER_01Продолжительность одновременной речиДоля одновременной речиКоличество перебиваний SPEAKER_00Количество перебиваний SPEAKER_01
0414.840-0.1669.06.012174218.52128.983018210.631853227.32127.887393240.657298500.1205281918
\n", + "
" + ], + "text/plain": [ + " Продолжительность разговора сек Тишина в начале сек Тишина в конце сек \\\n", + "0 414.84 0 -0.16 \n", + "\n", + " Длителньость тишины сек Доля тишины Продолжительность речи SPEAKER_00 \\\n", + "0 69.0 6.012174 218.52 \n", + "\n", + " Скорость речи SPEAKER_00 Количество интервалов SPEAKER_00 \\\n", + "0 128.983018 21 \n", + "\n", + " Доля речи SPEAKER_00 Продолжительность речи SPEAKER_01 \\\n", + "0 0.631853 227.32 \n", + "\n", + " Скорость речи SPEAKER_01 Количество интервалов SPEAKER_01 \\\n", + "0 127.887393 24 \n", + "\n", + " Доля речи SPEAKER_01 Продолжительность одновременной речи \\\n", + "0 0.657298 50 \n", + "\n", + " Доля одновременной речи Количество перебиваний SPEAKER_00 \\\n", + "0 0.120528 19 \n", + "\n", + " Количество перебиваний SPEAKER_01 \n", + "0 18 " + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pd.DataFrame(report)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Analysis of emotions by speech" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "from expert.core.congruence import audio_emotions" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": {}, + "outputs": [], + "source": [ + "speakers = example_utils.get_rounded_intervals(speakers)" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": {}, + "outputs": [], + "source": [ + "CoAudio = audio_emotions.audio_analysis.AudioAnalysis(video_path=file_path,\n", + " stamps=speakers,\n", + " speaker=\"SPEAKER_00\",\n", + " sr=44100)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Emotions of the first speaker" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[W NNPACK.cpp:53] Could not initialize NNPACK! Reason: Unsupported hardware.\n" + ] + } + ], + "source": [ + "audio_0_emotions = CoAudio.predict()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Emotions of the second speaker" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": {}, + "outputs": [], + "source": [ + "CoAudio = audio_emotions.audio_analysis.AudioAnalysis(video_path=file_path,\n", + " stamps=speakers,\n", + " speaker=\"SPEAKER_01\",\n", + " sr=44100)\n", + "audio_1_emotions = CoAudio.predict()" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": {}, + "outputs": [], + "source": [ + "emo_audio_0 = pd.DataFrame(audio_0_emotions)\n", + "emo_audio_1 = pd.DataFrame(audio_1_emotions)" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": {}, + "outputs": [], + "source": [ + "anger_speaker_1 = emo_audio_1.loc[(emo_audio_1['audio_anger'] > emo_audio_1['audio_neutral']) & (emo_audio_1['audio_anger'] > emo_audio_1['audio_happiness'])]\n", + "happy_speaker_1 = emo_audio_1.loc[(emo_audio_1['audio_happiness'] > emo_audio_1['audio_neutral']) & (emo_audio_1['audio_happiness'] > emo_audio_1['audio_anger'])]\n", + "\n", + "anger_speaker_0 = emo_audio_0.loc[(emo_audio_0['audio_anger'] > emo_audio_0['audio_neutral']) & (emo_audio_0['audio_anger'] > emo_audio_0['audio_happiness'])]\n", + "happy_speaker_0 = emo_audio_0.loc[(emo_audio_0['audio_happiness'] > emo_audio_0['audio_neutral']) & (emo_audio_0['audio_happiness'] > emo_audio_0['audio_anger'])]" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "metadata": {}, + "outputs": [], + "source": [ + "from termcolor import colored\n", + "\n", + "from datetime import datetime" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Show sentences of the first speaker said with aggression" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "03:03 \u001b[31m Сейчас по комиссии.\u001b[0m\n" + ] + } + ], + "source": [ + "for time in anger_speaker_0['time_sec']:\n", + " for elem in sentences[0]:\n", + " if sentences[0][elem][1][1] == 'SPEAKER_00' and sentences[0][elem][1][0][0] <= time and sentences[0][elem][1][0][1] >= time:\n", + " print(datetime.fromtimestamp(time).strftime(\"%M:%S\"), ' ', colored(sentences[0][elem][0], 'red'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Show sentences of the first speaker said with happiness" + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "01:32 \u001b[32m услуг нового.\u001b[0m\n", + "02:32 \u001b[32m скачать это приложение. И, конечно, это не так. Я живу. Какие у меня еще варианты есть? В отделении банка. В отделении банка спрашивают, вам помогут активировать вам все. Скажите, в любой отделении или? Ну, думаю, во всех отделениях будет именно планшет, чтобы вам с их устройства помогут активировать.\u001b[0m\n", + "02:43 \u001b[32m\u001b[0m\n", + "03:08 \u001b[32m Я не очень поняла, когда\u001b[0m\n", + "03:13 \u001b[32m не очень поняла, когда просто пустая карта смысл в ее. Но вроде как, какая-то сумма должна быть или что-то там должно покупаться с этой карты.\u001b[0m\n", + "03:53 \u001b[32m За расчет\u001b[0m\n", + "04:17 \u001b[32m Ради того, чтобы платить не 30, а 15 рублей, нужно в месяц тратить 500 рублей. Такая замануха, ну, прямо смешно даже иногда. По логике, да? То есть, я соглашаю. Ну, и то меня я пришла выяснить один вопрос. А один. Давайте, давайте. Я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Ну, если... Ну, если... Ну, вы согласитесь по человеку. Если вы отойдете от всех ваших дел банковских, просто по-человечески, принцип такой, чтобы тратить вам 15, а не 30, пожалуйста, тратите с карточки 500. Ну, если тратить именно... Я туда же до них положу еще.\u001b[0m\n", + "04:27 \u001b[32m Ради того, чтобы платить не 30, а 15 рублей, нужно в месяц тратить 500 рублей. Такая замануха, ну, прямо смешно даже иногда. По логике, да? То есть, я соглашаю. Ну, и то меня я пришла выяснить один вопрос. А один. Давайте, давайте. Я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Ну, если... Ну, если... Ну, вы согласитесь по человеку. Если вы отойдете от всех ваших дел банковских, просто по-человечески, принцип такой, чтобы тратить вам 15, а не 30, пожалуйста, тратите с карточки 500. Ну, если тратить именно... Я туда же до них положу еще.\u001b[0m\n", + "04:37 \u001b[32m Ради того, чтобы платить не 30, а 15 рублей, нужно в месяц тратить 500 рублей. Такая замануха, ну, прямо смешно даже иногда. По логике, да? То есть, я соглашаю. Ну, и то меня я пришла выяснить один вопрос. А один. Давайте, давайте. Я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Ну, если... Ну, если... Ну, вы согласитесь по человеку. Если вы отойдете от всех ваших дел банковских, просто по-человечески, принцип такой, чтобы тратить вам 15, а не 30, пожалуйста, тратите с карточки 500. Ну, если тратить именно... Я туда же до них положу еще.\u001b[0m\n", + "04:47 \u001b[32m Ради того, чтобы платить не 30, а 15 рублей, нужно в месяц тратить 500 рублей. Такая замануха, ну, прямо смешно даже иногда. По логике, да? То есть, я соглашаю. Ну, и то меня я пришла выяснить один вопрос. А один. Давайте, давайте. Я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Но, кстати, я не знаю, что это такое. Ну, если... Ну, если... Ну, вы согласитесь по человеку. Если вы отойдете от всех ваших дел банковских, просто по-человечески, принцип такой, чтобы тратить вам 15, а не 30, пожалуйста, тратите с карточки 500. Ну, если тратить именно... Я туда же до них положу еще.\u001b[0m\n", + "06:33 \u001b[32m скидки то уже нет. Ну, так я с вами говорю. Пока это есть, да, какие-то плюшки я выигрываю, но потом же все равно приходит к этому принципу. В общем, из того, что вы мне сказать, в любом банке они мне помогут ее активировать. И все.\u001b[0m\n" + ] + } + ], + "source": [ + "for time in happy_speaker_0['time_sec']:\n", + " for elem in sentences[0]:\n", + " if sentences[0][elem][1][1] == 'SPEAKER_00' and sentences[0][elem][1][0][0] <= time and sentences[0][elem][1][0][1] >= time:\n", + " print(datetime.fromtimestamp(time).strftime(\"%M:%S\"), ' ', colored(sentences[0][elem][0], 'green'))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### An example of the result obtained with AutoTM analysis for every speaker" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "result = pd.read_csv('./AutoTM/src/result.csv')" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "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", + "
speakerstopic_1topic_2
0speaker_00Тарифный план по обслуживанию картыБонусы при использовании карты
1speaker_01Тарифный план по обслуживанию картыБонусы при использовании карты
\n", + "
" + ], + "text/plain": [ + " speakers topic_1 \\\n", + "0 speaker_00 Тарифный план по обслуживанию карты \n", + "1 speaker_01 Тарифный план по обслуживанию карты \n", + "\n", + " topic_2 \n", + "0 Бонусы при использовании карты \n", + "1 Бонусы при использовании карты " + ] + }, + "execution_count": 76, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result[['speakers', 'topic_1', 'topic_2']]" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "relevant", + "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.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/cases/call_stats.py b/examples/cases/call_stats.py new file mode 100755 index 0000000..fdba723 --- /dev/null +++ b/examples/cases/call_stats.py @@ -0,0 +1,240 @@ +import librosa +from heapq import merge +from datetime import datetime + + +class DialogStats: + def __init__(self, audio, sr, annotation, speakers): + self.audio = audio + self.sr = sr + self.annotation = annotation + self.speakers = speakers + + # Извлечение интервалов + def get_timestamps(self): + self.timestamps = [] + for elem in self.annotation: + self.timestamps.append(self.annotation[elem][1][0]) + + # Когда была речь, в целом + def non_overlapping_intervals(self): + self.non_overlapping = [] + starts = sorted( + [(i[0], 1) for i in self.timestamps] + ) # start of interval adds a layer of overlap + ends = sorted([(i[1], -1) for i in self.timestamps]) # end removes one + layers = 0 + current = [] + for value, event in merge( + starts, ends + ): # sorted by value, then ends (-1) before starts (1) + layers += event + if layers == 1: # start of a new non-overlapping interval + current.append(value) + elif ( + current + ): # we either got out of an interval, or started an overlap + current.append(value) + self.non_overlapping.append(current) + current = [] + return self.non_overlapping + + # Перебивания + def get_intersections(self): + def _get_intersection(interval1, interval2): + new_min = max(interval1[0], interval2[0]) + new_max = min(interval1[1], interval2[1]) + return [new_min, new_max] if new_min <= new_max else None + + self.interval_intersection = [ + x + for x in ( + _get_intersection(y, z) + for y in self.speakers["SPEAKER_01"] + for z in self.speakers["SPEAKER_00"] + ) + if x is not None + ] + + # Продолжительность разговора + def call_duration(self): + if len(self.audio.shape) == 2: + self.duration = self.audio.shape[1] / self.sr + elif len(self.audio.shape) == 1: + self.duration = self.audio.shape[0] / self.sr + else: + raise TypeError + + # Продолжительность тишины в начале/конце разговора + def start_end_silence(self): + """ + Example of annotation's element {0: (' поступила заявка от вашего личного кабинета', + ([1.257, 4.497], 'SPEAKER_01'))} + """ + silence_in_the_start = self.annotation[0][1][0][0] + self.last_key = list(self.annotation)[ + -1 + ] # index of the last annotation record + silence_in_the_end = ( + self.duration - self.annotation[self.last_key][1][0][1] + ) + return silence_in_the_start, silence_in_the_end + + # Длительность речи. Считается как общая (не пересекающиеся интервалы), так и для каждого спикера/одновременной речи + def get_voice_duration(self, speaker=None, intersactions=None): + if speaker: + intervals = self.speakers[speaker] + elif intersactions: + intervals = self.interval_intersection + else: + intervals = self.non_overlapping + voice_duration = 0 + for interval in intervals: + voice_duration += interval[1] - interval[0] + + return voice_duration + + # Максимальное значение тишины + def silence_info(self): + silences = [] + for i in range(1, len(self.non_overlapping)): + silences.append( + self.non_overlapping[i][0] - self.non_overlapping[i - 1][1] + ) + + silence_part = ( + self.duration - self.get_voice_duration() + ) / self.duration + return max(silences), silence_part + + # Средняя скорость речи выбранного спикера + def get_tempo(self, speaker): + each_temp = [] + if len(self.audio.shape) == 2: + audio = self.audio[0] + else: + audio = self.audio + for interval in self.speakers[speaker]: + fragment = audio[ + int(self.sr * interval[0]) : int(self.sr * interval[1]) + ].numpy() + onset_env = librosa.onset.onset_strength(fragment, sr=self.sr) + tempo = librosa.beat.tempo(onset_envelope=onset_env, sr=self.sr) + each_temp.append(tempo) + return float((sum(each_temp) / len(each_temp))) + + def interrupt_info(self): + speaker_0_interrupt = 0 + speaker_1_interrupt = 0 + for interval in self.interval_intersection: + for elem in self.speakers["SPEAKER_00"]: + # Перебивает спикер 1 + if interval[0] <= elem[1] and interval[0] > elem[0]: + print("Перебивал SPEAKER_01") + print( + datetime.fromtimestamp(interval[0]).strftime("%M:%S") + + " - " + + datetime.fromtimestamp(interval[1]).strftime("%M:%S") + ) + speaker_1_interrupt += 1 + for elem in self.speakers["SPEAKER_01"]: + # Перебивает спикер 0 + if interval[0] <= elem[1] and interval[0] > elem[0]: + print("Перебивал SPEAKER_00") + print( + datetime.fromtimestamp(interval[0]).strftime("%M:%S") + + " - " + + datetime.fromtimestamp(interval[1]).strftime("%M:%S") + ) + speaker_0_interrupt += 1 + return speaker_0_interrupt, speaker_1_interrupt + + +def call_statistic(audio, sr, annotation, speakers): + report = {} + dialStats = DialogStats(audio, sr, annotation, speakers) + dialStats.get_timestamps() + dialStats.call_duration() + dialStats.get_intersections() + dialStats.non_overlapping_intervals() + report.update({"Продолжительность разговора сек": [dialStats.duration]}) + start_silence, end_silence = dialStats.start_end_silence() + report.update({"Тишина в начале сек": [start_silence]}) + report.update({"Тишина в конце сек": [end_silence]}) + report.update( + { + "Длителньость тишины сек": [ + dialStats.duration - dialStats.get_voice_duration() + ] + } + ) + report.update( + { + "Доля тишины": [ + dialStats.duration + / (dialStats.duration - dialStats.get_voice_duration()) + ] + } + ) + report.update( + { + "Продолжительность речи SPEAKER_00": [ + dialStats.get_voice_duration(speaker="SPEAKER_00") + ] + } + ) + report.update( + {"Скорость речи SPEAKER_00": [dialStats.get_tempo("SPEAKER_00")]} + ) + report.update( + {"Количество интервалов SPEAKER_00": [len(speakers["SPEAKER_00"])]} + ) + report.update( + { + "Доля речи SPEAKER_00": [ + dialStats.get_voice_duration("SPEAKER_00") + / dialStats.get_voice_duration() + ] + } + ) + report.update( + { + "Продолжительность речи SPEAKER_01": [ + dialStats.get_voice_duration(speaker="SPEAKER_01") + ] + } + ) + report.update( + {"Скорость речи SPEAKER_01": [dialStats.get_tempo("SPEAKER_01")]} + ) + report.update( + {"Количество интервалов SPEAKER_01": [len(speakers["SPEAKER_01"])]} + ) + report.update( + { + "Доля речи SPEAKER_01": [ + dialStats.get_voice_duration("SPEAKER_01") + / dialStats.get_voice_duration() + ] + } + ) + report.update( + { + "Продолжительность одновременной речи": [ + dialStats.get_voice_duration(intersactions=True) + ] + } + ) + report.update( + { + "Доля одновременной речи": [ + dialStats.get_voice_duration(intersactions=True) + / dialStats.duration + ] + } + ) + speaker_0_interrupt, speaker_1_interrupt = dialStats.interrupt_info() + report.update({"Количество перебиваний SPEAKER_00": [speaker_1_interrupt]}) + report.update({"Количество перебиваний SPEAKER_01": [speaker_0_interrupt]}) + + return report diff --git a/examples/cases/example_utils.py b/examples/cases/example_utils.py new file mode 100644 index 0000000..f3e28c7 --- /dev/null +++ b/examples/cases/example_utils.py @@ -0,0 +1,103 @@ +import torch + + +def separate_marks_for_speakers(dict_with_marks): + speakers = {} + for mark in dict_with_marks: + if mark["speaker"] not in speakers: + speakers.update({mark["speaker"]: []}) + + for speaker in speakers.keys(): + for mark in dict_with_marks: + if mark["speaker"] == speaker: + speakers[speaker].append([mark["start"], mark["finish"]]) + + return speakers + + +def create_separated_signals(signal, speakers_info, name, sr=16000): + first = signal[0][ + int(speakers_info[name][0][0] * sr) : int( + speakers_info[name][0][1] * sr + ) + ] + for num in range(1, len(speakers_info[name])): + first = torch.concat( + ( + first, + signal[0][ + int(speakers_info[name][num][0] * sr) : int( + speakers_info[name][num][1] * sr + ) + ], + ) + ) + + return first + + +def get_rounded_intervals(stamps): + for speaker in stamps: + for interval in stamps[speaker]: + interval[0] = int(interval[0]) # math.floor + interval[1] = int(-(-interval[1] // 1)) # math.ceil + + return stamps + + +def merge_intervals(stamps): + for speaker in stamps: + intervals = stamps[speaker] + # Merge overlapped intervals. + stack = [] + # Insert first interval into stack. + stack.append(intervals[0]) + for i in intervals[1:]: + # Check for overlapping interval. + if stack[-1][0] <= i[0] <= stack[-1][-1]: + stack[-1][-1] = max(stack[-1][-1], i[-1]) + else: + stack.append(i) + stamps[speaker] = stack + + return stamps + + +def sentences_with_time(timestamps, words): + sentences = dict.fromkeys(range(len(timestamps)), "") + _words = words[:] + empty = [] + while _words: + w = _words.pop(0) + status = False + for i in range(len(timestamps)): + if ( + timestamps[i][0][0] <= w["start"] + and w["start"] <= timestamps[i][0][1] + ): + sentences[i] += " " + w["text"] + status = True + break + else: + status = False + if not status: + empty.append([w["text"], w["start"]]) + for elem in sentences: + sentences[elem] = (sentences[elem], timestamps[elem]) + return sentences, empty + + +def place_words(timestamps, empty): + stamp_starts = [t[0][0] for t in timestamps] + closest_idxs = [(99, 1)] * len(empty) + for i in range(len(empty)): + for j in range(len(stamp_starts)): + if ( + min(closest_idxs[i][0], abs(stamp_starts[j] - empty[i][1])) + != closest_idxs[i][0] + ): + closest_idxs[i] = ( + min(closest_idxs[i][0], abs(stamp_starts[j] - empty[i][1])), + j, + ) + return closest_idxs diff --git a/expert/core/congruence/congruence_analysis.py b/expert/core/congruence/congruence_analysis.py index 44c09a0..c10b8ca 100644 --- a/expert/core/congruence/congruence_analysis.py +++ b/expert/core/congruence/congruence_analysis.py @@ -198,11 +198,10 @@ def get_congruence(self): emotions_data["audio"] = audio_data.to_dict(orient="records") emotions_data["text"] = text_data.to_dict(orient="records") - if self.return_path: - with open( - os.path.join(self.temp_path, "emotions.json"), "w" - ) as filename: - json.dump(emotions_data, filename) + with open( + os.path.join(self.temp_path, "emotions.json"), "w" + ) as filename: + json.dump(emotions_data, filename) cong_data[["video_path", "time_sec", "congruence"]].to_json( os.path.join(self.temp_path, "congruence.json"), @@ -210,7 +209,6 @@ def get_congruence(self): ) if self.return_path: - return os.path.join(self.temp_path, "emotions.json"), os.path.join( self.temp_path, "congruence.json" )