Skip to content

392 Compare student speech whole text #406

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions app/criteria/comparison_speech_slides/criterion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from bson import ObjectId

from app.root_logger import get_root_logger
from app.localisation import *
from ..criterion_base import BaseCriterion
from ..criterion_result import CriterionResult
from app.audio import Audio
from app.presentation import Presentation
from app.utils import normalize_text, delete_punctuation
from ..text_comparison import SlidesSimilarityEvaluator

logger = get_root_logger('web')


# Критерий, оценивающий, насколько текст слайда перекликается с речью студента на этом слайде
class ComparisonSpeechSlidesCriterion(BaseCriterion):
PARAMETERS = dict(
skip_slides=list.__name__,
)

def __init__(self, parameters, dependent_criteria, name=''):
super().__init__(
name=name,
parameters=parameters,
dependent_criteria=dependent_criteria,
)
self.evaluator = SlidesSimilarityEvaluator()
if 'slide_speech_threshold' not in self.parameters:
self.parameters['slide_speech_threshold'] = 0.125

@property
def description(self):
return {
"Критерий": t(self.name),
"Описание": t(
"Проверяет, что текст слайда соответствует словам, которые произносит студент во время демонстрации "
"этого слайда"),
"Оценка": t("1, если среднее значение соответствия речи содержимому слайдов равно или превосходит заданного порога (от 0 до 1), "
"иначе r / значение порога, где r - среднее значение соответствия речи демонстрируемым слайдам")
}

def skip_slide(self, current_slide_text: str) -> bool:
for skip_slide in self.parameters['skip_slides']:
if skip_slide.lower() in delete_punctuation(current_slide_text).lower():
return True
return False

def apply(self, audio: Audio, presentation: Presentation, training_id: ObjectId,
criteria_results: dict) -> CriterionResult:
# Результаты сравнения текстов
results = {}

slides_to_process = []

for current_slide_index in range(len(audio.audio_slides)):
# Список слов, сказанных студентом на данном слайде -- список из RecognizedWord
current_slide_speech = audio.audio_slides[current_slide_index].recognized_words
# Удаление time_stamp-ов и probability, ибо работа будет вестись только со словами
current_slide_speech = list(map(lambda x: x.word.value, current_slide_speech))
# Нормализация текста выступления
current_slide_speech = " ".join(normalize_text(current_slide_speech))

# Если на данном слайде ничего не сказано, то не обрабатываем данный слайд
if len(current_slide_speech.split()) == 0:
results[current_slide_index + 1] = 0.000
continue

# Список слов со слайда презентации
current_slide_text = presentation.slides[current_slide_index].words
# Проверяем, входит ли рассматриваемый слайд в список нерасмматриваемых
if self.skip_slide(current_slide_text):
logger.info(f"Слайд №{current_slide_index + 1} пропущен")
continue

# Нормализация текста слайда
current_slide_text = " ".join(normalize_text(current_slide_text.split()))
slides_to_process.append((current_slide_speech, current_slide_text, current_slide_index + 1))

self.evaluator.train_model([" ".join(list(map(lambda x: x[0], slides_to_process))), " ".join(list(map(lambda x: x[1], slides_to_process)))])

for speech, slide_text, slide_number in slides_to_process:
results[slide_number] = self.evaluator.evaluate_semantic_similarity(speech, slide_text)

results = dict(sorted(results.items()))

score = (sum(list(results.values())) / len(list(results.values()))) / self.parameters['slide_speech_threshold']

return CriterionResult(1 if score >= 1 else score, "Отлично" if score >= 1 else "Следует уделить внимание "
"соотвествию речи на слайдах "
"{}".format(",\n".join([f"№{n} - {results[n]}" for n in dict(filter(lambda item: item[1] < self.parameters['slide_speech_threshold'], results.items()))])))
84 changes: 84 additions & 0 deletions app/criteria/comparison_whole_speech/criterion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from bson import ObjectId

from app.root_logger import get_root_logger
from app.localisation import *
from ..criterion_base import BaseCriterion
from ..criterion_result import CriterionResult
from app.audio import Audio
from app.presentation import Presentation
from app.utils import normalize_text
from ..text_comparison import Doc2VecEvaluator

logger = get_root_logger('web')


class ComparisonWholeSpeechCriterion(BaseCriterion):
PARAMETERS = dict(
vector_size=int.__name__,
window=int.__name__,
min_count=int.__name__,
workers=int.__name__,
epochs=int.__name__,
dm=int.__name__,
)

def __init__(self, parameters, dependent_criteria, name=''):
super().__init__(
name=name,
parameters=parameters,
dependent_criteria=dependent_criteria,
)
vector_size = self.parameters['vector_size']
window = self.parameters['window']
min_count = self.parameters['min_count']
workers = self.parameters['workers']
epochs = self.parameters['epochs']
dm = self.parameters['dm']

self.model = Doc2VecEvaluator(vector_size, window, min_count, workers, epochs, dm)

@property
def description(self):
return {
"Критерий": t(self.name),
"Описание": t("Проверяет, что тема доклада студента совпадает с темой презентации"),
"Оценка": t(
"1, если тема доклада и презентации совпадают не менее, чем на 40%, иначе 2.5 * k, где k - степень соответствия темы доклада теме презентации")
}

def apply(self, audio: Audio, presentation: Presentation, training_id: ObjectId,
criteria_results: dict) -> CriterionResult:
normalized_speech = []
normalized_slides = []

for i in range(len(audio.audio_slides)):
# Список сказанных на слайде слов
current_slide_speech = audio.audio_slides[i].recognized_words
# Очистка списка от timestamp-ов и probability
current_slide_speech = list(map(lambda x: x.word.value, current_slide_speech))
# Нормализация текста
current_slide_speech = " ".join(normalize_text(current_slide_speech))
if current_slide_speech != "":
normalized_speech.append(current_slide_speech)

# Текст из слайда презентации
current_slide_text = presentation.slides[i].words
# Нормализация текста слайда
current_slide_text = " ".join(normalize_text(current_slide_text.split()))
if current_slide_text != "":
normalized_slides.append(current_slide_text)

if len(normalized_speech) == 0:
return CriterionResult(0, "Тренажер не зафиксировал, что вы что-то говорили")
normalized_speech_text = " ".join(normalized_speech)

if len(normalized_slides) == 0:
return CriterionResult(0, "Загруженная вами презентация не содержит текста")
normalized_slides_text = " ".join(normalized_slides)

self.model.train_model([normalized_speech_text, normalized_slides_text])

score = 2.5 * self.model.evaluate_semantic_similarity(normalized_speech_text, normalized_slides_text)
logger.info(f"Score={score}")
return CriterionResult(1 if score >= 1 else score,
"Ваша речь соответствует тексту презентации" if score >= 1 else "Ваша речь не полностью соответствует теме презентации")
2 changes: 2 additions & 0 deletions app/criteria/criterions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@
from .speech_is_not_in_database.criterion import SpeechIsNotInDatabaseCriterion
from .speech_pace.criterion import SpeechPaceCriterion
from .strict_speech_duration.criterion import StrictSpeechDurationCriterion
from .comparison_speech_slides.criterion import ComparisonSpeechSlidesCriterion
from .comparison_whole_speech.criterion import ComparisonWholeSpeechCriterion
25 changes: 23 additions & 2 deletions app/criteria/preconfigured_criterions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@

from criteria import (FillersNumberCriterion, FillersRatioCriterion,
SpeechIsNotInDatabaseCriterion, SpeechPaceCriterion,
StrictSpeechDurationCriterion)
StrictSpeechDurationCriterion, ComparisonSpeechSlidesCriterion,
ComparisonWholeSpeechCriterion)

from .utils import DEFAULT_FILLERS

from .utils import DEFAULT_SKIP_SLIDES

preconfigured_criterions = [
# SpeechDurationCriterion
Expand Down Expand Up @@ -143,7 +144,27 @@
}
},
dependent_criteria=[],
),

ComparisonSpeechSlidesCriterion(
name="ComparisonSpeechSlidesCriterion",
parameters={"skip_slides": DEFAULT_SKIP_SLIDES},
dependent_criteria=[],
),

ComparisonWholeSpeechCriterion(
name="ComparisonWholeSpeechCriterion",
parameters={
"vector_size": 200,
"window": 5,
"min_count": 3,
"workers": 4,
"epochs": 40,
"dm": 0
},
dependent_criteria=[],
)

]


Expand Down
37 changes: 37 additions & 0 deletions app/criteria/text_comparison.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from gensim.models.doc2vec import Doc2Vec, TaggedDocument


class SlidesSimilarityEvaluator:
def __init__(self):
self.vectorizer = TfidfVectorizer(ngram_range=(1, 1))

def train_model(self, corpus: list):
self.vectorizer.fit(corpus)

def evaluate_semantic_similarity(self, text1: str, text2: str) -> float:
vector1 = self.vectorizer.transform([text1])
vector2 = self.vectorizer.transform([text2])
similarity = cosine_similarity(vector1, vector2)[0][0]

return round(similarity, 3)


class Doc2VecEvaluator:
def __init__(self, vector_size: int, window: int, min_count: int, workers: int, epochs: int, dm: int):
self.model = Doc2Vec(vector_size=vector_size, window=window, min_count=min_count, workers=workers,
epochs=epochs, dm=dm)

def train_model(self, documents: list):
tagged_documents = [TaggedDocument(words=doc.split(), tags=[i]) for i, doc in enumerate(documents)]
self.model.build_vocab(tagged_documents)
self.model.train(tagged_documents, total_examples=self.model.corpus_count, epochs=self.model.epochs)

def evaluate_semantic_similarity(self, text1: str, text2: str) -> float:
text1 = text1.split()
text2 = text2.split()

similarity = self.model.wv.n_similarity(text1, text2)

return round(similarity, 3)
5 changes: 4 additions & 1 deletion app/criteria/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import traceback
from typing import Optional, Callable


from app.audio import Audio
from app.utils import get_types

Expand Down Expand Up @@ -84,6 +83,10 @@ def get_fillers_number(fillers: list, audio: Audio) -> int:
return sum(map(len, get_fillers(fillers, audio)))


DEFAULT_SKIP_SLIDES = [
"Спасибо за внимание",
]

DEFAULT_FILLERS = [
'короче',
'однако',
Expand Down
5 changes: 4 additions & 1 deletion app/criteria_pack/preconfigured_pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@
['DEFAULT_FILLERS_RATIO_CRITERION', 0.33]],
'SlidesCheckerPack':
[['SimpleNumberSlidesCriterion', 0.05],
['SlidesCheckerCriterion', 0.95]]
['SlidesCheckerCriterion', 0.95]],
'ComparisonPack':
[['ComparisonSpeechSlidesCriterion', 0.5],
['ComparisonWholeSpeechCriterion', 0.5]]
}


Expand Down
2 changes: 1 addition & 1 deletion app/feedback_evaluator.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json

from app.criteria import SpeechDurationCriterion, SpeechPaceCriterion, FillersRatioCriterion, FillersNumberCriterion, \
StrictSpeechDurationCriterion
StrictSpeechDurationCriterion, ComparisonSpeechSlidesCriterion, ComparisonWholeSpeechCriterion


class Feedback:
Expand Down
2 changes: 0 additions & 2 deletions app/presentation_parser/slide_splitter.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import fitz
import pymorphy2
import nltk
nltk.download('stopwords')
from nltk.corpus import stopwords

import os
Expand Down
4 changes: 4 additions & 0 deletions app/training_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ def run(self):


if __name__ == "__main__":
import nltk
nltk.download('stopwords')
nltk.download('punkt')

Config.init_config(sys.argv[1])
training_processor = TrainingProcessor()
training_processor.run()
Loading
Loading