diff --git a/backend/app/analysis/text_parser.py b/backend/app/analysis/text_parser.py new file mode 100644 index 0000000..99eccb4 --- /dev/null +++ b/backend/app/analysis/text_parser.py @@ -0,0 +1,165 @@ +""" +Custom command to populate text model with definition, examples, and images +""" +import string +import nltk +import Levenshtein as Lev + + +# Modified Code from Bing library +def get_sentences(text): + """ + This function, when given a string of words, breaks it apart into individual + sentences through markers of sentence breaks and line breaks(\n) + + :param text: str, any text + :return: list containing the fragments of the broken down text + """ + # breaks up text by line breaks first + lines = [p for p in text.split('\n') if p] + sentences = [] + + # breaks up each line by sentence markers + for line in lines: + sentences.extend(nltk.tokenize.sent_tokenize(line)) + + return sentences + + +def correct_sentence(given_sent, correct_sent): + """ + Function takes the user's sentence and the correct sentence and compares them to find missing + words, the incorrect words, and a grade + + :param given_sent: str, sentence the user types into the text box + :param correct_sent: str, the correct sentence that the instructor inputs + :return: dictionary with parameters holding the missing words, correct words, + word/correctness at each index + """ + + grade = {} + + # tokenize given_sent and correct_sent and turn them both into lists + given_tok = nltk.tokenize.word_tokenize(given_sent.lower().translate( + str.maketrans('', '', string.punctuation))) + correct_tok = nltk.tokenize.word_tokenize(correct_sent.lower().translate( + str.maketrans('', '', string.punctuation))) + + # hold all the correct words and removed every word that is found to hold only + # the missing words + grade["missing"] = correct_tok.copy() + + for word in given_tok: + if grade["missing"].count(word) != 0: + # remove the words that the user typed in to get the missing words + grade["missing"].remove(word) + + + grade["incorrect_word_index"] = [] + + # True if the user inputs the same sentence as the correct answer + grade["isCorrect"] = given_tok == correct_tok + + grade["words"] = [] + + index = 0 + for ind_1, word in enumerate(given_tok): + match_found = False + + # loop until the match is found or all words are looked at and no match is found + for ind_2, match in enumerate(correct_tok[index:]): + + if word == match: + match_found = True + match_index = ind_2 + index + break + + # if the match is found then the grade is correct, incorrect otherwise + # increase match_index by 1 if the match is found + if match_found: + grade["words"].append({"word": word, + "grade": "correct"}) + index = match_index + 1 + else: + grade["incorrect_word_index"].append(ind_1) + grade["words"].append({"word": word, + "grade": "incorrect"}) + + # incorrect indices + for word_index in grade["incorrect_word_index"]: + + # find the most similar word + sim_word = most_similar_word(given_tok[word_index], grade["missing"]) + + if sim_word is not None: + word_grade = correct_words(given_tok[word_index], sim_word) + grade["words"][word_index]["word_grade"] = word_grade + + return grade + + +def most_similar_word(word, comparisons): + """ + Take a word and find the most similar word in a given list + + :param word: string with one word + :param comparisons: list of words + :return: string, most similar word in comparisons to word + """ + min_lev_val = None + min_lev_word = None + + # check the missing words and see how similar they are + for current_word in comparisons: + current_val = Lev.distance(word, current_word) + + # see if the word is less or more similar + if min_lev_val is None or min_lev_val > current_val: + min_lev_val = current_val + min_lev_word = current_word + + # return the most similar word + return min_lev_word + + +def correct_words(given_word, correct_word): + """ + Determine which letters in a input word are correct and incorrect + + :param given_word: string with one word, the incorrect word from the user's input + :param correct_word: string with one word, the similar word that is correct + :return: + """ + grade = {} + + char_missing = [] + + char_missing[:0] = correct_word + + # remove the character if the character is there, find out how many are missing + for char in given_word: + if char_missing.count(char) != 0: + char_missing.remove(char) + + grade["missing"] = list(char_missing) + grade["letters"] = [] + + # for each character, hold whether it is correct or incorrect + index = 0 + for char in given_word: + match_found = False + for ind_2, match in enumerate(correct_word[index:]): + + if char == match: + match_found = True + match_index = ind_2 + index + break + if match_found: + grade["letters"].append({"char": char, + "grade": "correct"}) + index = match_index + 1 + else: + grade["letters"].append({"char": char, + "grade": "incorrect"}) + + return grade diff --git a/backend/app/views.py b/backend/app/views.py index ed23e05..0474bb8 100644 --- a/backend/app/views.py +++ b/backend/app/views.py @@ -32,7 +32,14 @@ from .analysis.crosswords import ( get_crosswords, ) -from .quiz_creation.conjugation_quiz import get_quiz_sentences +from .quiz_creation.conjugation_quiz import ( + get_quiz_sentences, +) +from .analysis.text_parser import ( + get_sentences, + correct_sentence, +) + @api_view(['GET']) @@ -243,3 +250,24 @@ def get_response_quiz_data(request, text_id): raise Http404 from text_not_exist res = get_quiz_questions(text_obj.content) return Response(res) + + +@api_view(['GET']) +def get_indiv_sentences(request, text_id): + """ + API endpoint for getting the individual sentences from the given text. + """ + text_obj = Text.objects.get(id=text_id) + sentences = get_sentences(text_obj.content) + res = [{'sentence': sentence} for sentence in sentences] + return Response(res) + + +@api_view(['GET']) +def get_sentence_grade(request, user_sent, actual_sent): + """ + API endpoint for getting the individual sentences from the given text. + """ + graded_sentence = correct_sentence(user_sent, actual_sent) + + return Response(graded_sentence) diff --git a/backend/config/urls.py b/backend/config/urls.py index fbdab93..b64f390 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -28,7 +28,10 @@ get_picturebook_data, get_crossword, get_quiz_data, - text, get_response_quiz_data, + text, + get_indiv_sentences, + get_sentence_grade, + get_response_quiz_data, ) @@ -61,6 +64,9 @@ def react_view_path(route, component_name): path('api/text/', text), path('api/get_picturebook_prompt//', get_picturebook_prompt), path('api/get_picturebook_data', get_picturebook_data), + path('api/get_indiv_sentences/', get_indiv_sentences), + path('api/get_sentence_grade//', get_sentence_grade), + # View paths react_view_path('', 'IndexView'), @@ -74,4 +80,5 @@ def react_view_path(route, component_name): react_view_path('picturebook//', 'PictureBookView'), react_view_path('response_quiz/', 'ResponseAllQuizView'), react_view_path('response_quiz//', 'ResponseQuizView'), + react_view_path('textToSpeech/', 'TextToSpeech'), ] diff --git a/frontend/src/UILibrary/styles.scss b/frontend/src/UILibrary/styles.scss index cdd7353..030e119 100644 --- a/frontend/src/UILibrary/styles.scss +++ b/frontend/src/UILibrary/styles.scss @@ -8,6 +8,7 @@ @import '../instructorView/instructorView'; @import '../flashcard/flashcardView'; @import '../pictureBookView/pictureBookView'; +@import '../textToSpeech/textToSpeech'; html { position: relative; diff --git a/frontend/src/app.js b/frontend/src/app.js index 291eddd..0b315fc 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -17,6 +17,7 @@ import { PictureBookView } from './pictureBookView/pictureBookView'; import { CrosswordView } from './crosswordView/crosswordView'; import { ResponseAllQuizView } from './responseQuizView/responseAllQuizView'; import { ResponseQuizView } from './responseQuizView/responseQuizView'; +import { TextToSpeech } from './textToSpeech/textToSpeech'; // Import all styles import './UILibrary/styles.scss'; @@ -36,4 +37,5 @@ window.app_modules = { QuizView, ResponseAllQuizView, ResponseQuizView, + TextToSpeech, }; diff --git a/frontend/src/index/index.js b/frontend/src/index/index.js index 917091b..530993d 100644 --- a/frontend/src/index/index.js +++ b/frontend/src/index/index.js @@ -14,6 +14,7 @@ const QUIZ_TYPES = { 'Quiz': ['quiz', idLink], 'Crossword': ['crossword', posLink], 'Story Generator': ['picturebook', posLink], + 'TextToSpeech': ['textToSpeech', idLink], }; class TextInfo extends React.Component { diff --git a/frontend/src/textToSpeech/textToSpeech.js b/frontend/src/textToSpeech/textToSpeech.js new file mode 100644 index 0000000..a219f6a --- /dev/null +++ b/frontend/src/textToSpeech/textToSpeech.js @@ -0,0 +1,380 @@ +import React, { Component } from 'react'; +import * as PropTypes from 'prop-types'; + +import { Footer, Navbar, LoadingPage } from '../UILibrary/components'; + +const playButton = (color) => { + return ( + + + + + ); +}; + +export class TextToSpeech extends Component { + constructor(props) { + super(props); + this.state = { + textData: null, + sentenceIndex: 0, + showModal: false, + userText: '', + graded: false, + grade: null, + continue: false, + showAnswer: false, + }; + this.modalHandler = this.modalHandler.bind(this); + } + + componentDidMount = async () => { + const apiURL = `/api/get_indiv_sentences/${this.props.textID}`; + const response = await fetch(apiURL); + const data = await response.json(); + this.setState({ + textData: data, + }); + console.log(this.state.textData); + document.addEventListener('keydown', this.handleKeyDown, true); + } + + componentWillUnmount() { + document.removeEventListener('keydown', this.handleKeyDown, false); + } + + getCurrentSentence = () => { + return this.state.textData[this.state.sentenceIndex]['sentence']; + } + + changeSentence = (delta) => { + const { textData, sentenceIndex } = this.state; + const listLength = textData.length; + const newsentenceIndex = (sentenceIndex + delta + listLength) % listLength; + this.setState({ + sentenceIndex: newsentenceIndex, + showNext: true, + }); + this.reset(); + } + + reset = async () => { + this.setState({ + showModal: false, + userText: '', + graded: false, + grade: null, + continue: false, + showAnswer: false, + }); + document.getElementById('content').value = ''; + } + + giveUp = () => { + this.setState({ + continue: true, + showAnswer: true, + }); + } + + playAudio = () => { + const utterance = new SpeechSynthesisUtterance(); + utterance.text = this.getCurrentSentence(); + utterance.lang = 'en-US'; + utterance.rate = 1; + speechSynthesis.speak(utterance); + } + + handleSubmit = () => { + const input = document.getElementById('content').value; + console.log('this is the input i want', input); + this.setState({ + userText: input, + }); + this.gradeText(input); + + console.log('You have submitted!!!'); + }; + + gradeText = async (input) => { + console.log('gradeText was called'); + try { + console.log('im trying something here'); + const apiURL = `/api/get_sentence_grade/${input}/${this.getCurrentSentence()}`; + const response = await fetch(apiURL); + const grade = await response.json(); + console.log(grade); + + this.setState({ grade: grade, graded: true }); + this.setState({ continue: grade['isCorrect'] }); + } catch (e) { + console.log(e); + } + } + + modalHandler = (event) => { + event.preventDefault(); + this.setState((prevState) => ({ + showModal: !prevState.showModal, + })); + } + + checkProgress = () => { + const numSentences = this.state.textData.length; + const currentSentence = this.state.sentenceIndex + 1; + if (numSentences !== 0) { + return parseInt((currentSentence / numSentences) * 100); + } + return 0; + } + + giveGrade = () => { + const words = []; + + for (let i = 0; i < this.state.grade['words'].length; i++) { + const word = this.state.grade['words'][i]['word']; + const grade = this.state.grade['words'][i]['grade']; + const isWordCorrect = grade === 'correct'; + const classNameString = isWordCorrect ? 'correct-word' : 'incorrect-word'; + words.push({word}); + words.push( ); + } + + return words; + } + + giveMissingWords = () => { + console.log('missing words was called'); + console.log(this.state.grade['missing']); + console.log(this.state.grade['missing'].length); + const words = []; + for (let i = 0; i < this.state.grade['missing'].length; i++) { + words.push( +

{this.state.grade['missing'][i]}

, + ); + } + + this.shuffle(words); + return words; + } + + shuffle = async (arr) => { + arr.sort(() => Math.random() - 0.5); + } + + render() { + const { + textData, + sentenceIndex, + } = this.state; + + if (!textData) { + return (); + } + + const sentenceLength = textData.length; + const progressText = sentenceLength === 0 + ? 'No Sentences Available' + : `${sentenceIndex + 1}/${sentenceLength} Words`; + + let missingWord = null; + if (this.state.graded) { + missingWord = this.giveMissingWords(); + } + + return ( + <> + +
+
+
+

Sentence

+ +
+
+ { + this.state.showModal + ?
+
+ : null + } +
+
+
Instructions
+ +
+
+

Click the play button to hear the + sentence. After, type exactly what you hear. + Press submit to see your score! Press give + up if you want the correct answer.

+
+
+ +
+
+
+ +
+
+ {progressText} +
+
+
+
+
+
+
+
+
+ {playButton('pink')} +
+
+ {null} +
+
+
+

Here is your grade:

+ { + this.state.graded + ? this.giveGrade() + :

You have not submitted yet

+ } + { + this.state.showAnswer + ? ( +
+
+

This is the correct answer:

+

{this.getCurrentSentence()}

+
+ ) + : null + } +
+
+
+

Missing Words:

+ { + this.state.graded + ? ( +
+
+ {missingWord.slice(0, + Math.round(missingWord.length / 2))} +
+
{ + missingWord.slice(Math.round(missingWord.length + / 2), missingWord.length)} +
+
+ ) + : null + } +
+
+
+
+
+
+
+ + +
+
+
+
+ +
+
+
+ { + this.state.continue + ? ( +
+ + +
+ + ) + : ( +
+ + +
+ ) + } +
+
+