# Lloyd Milligan Practice solving day-of-week name for spoken pseudo-random dates # License cc 4.0 (attribution) - https://creativecommons.org/licenses/by/4.0/ # import sys import os from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import Qt # For timer import random from random import randint from datetime import datetime, date import time from gtts import gTTS from pydub import AudioSegment import simpleaudio as sa import speech_recognition as sr import re # currentDate = '' # mm/dd/yyyy format - Compatible with previous written practice application correctDayNumber = -1 # Day number (0 - 6) corresponding to currentDate (lowercase) scored = False # Score each challenge date just once. If true do not repeat feedback. difficulty = -1 # Numeric subjective difficulty estimate for current challenge date - hardnessScore() spCurrentDate = '' # Current test date string formatted as 'mm, dd, yy yy' magicDay = [3, 28, 7, 4, 9, 6, 11, 8, 5, 10, 7, 12] # AKA "Doomsday" (Conway) answerCheckingTimerInterval = 100 # milliseconds okToCheckAnswer = True # Kludge - Prevent interrupting to check answer while checking is in progress lowercaseDays = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'] monthName = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] wrongAnswer = list() # Wrong answers to current challenge date startSolve = datetime.now() # Value will be overwritten endSolve = startSolve # Ditto numToName = ['One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten'] leapYearDates = list() # Leap years with Jan or Feb months, i.e. tricky ones (for analysis) positiveClip1 = ['Very good', 'Excellent', 'Good work', 'Good going', 'Nice work', 'You got it'] positiveClip2 = ['Keep it up', 'Try another', 'Have another go', 'Congratulations', "That's correct"] listening = False # Do not start listener if it is already listening listeningContext = False # Coordinate with solve, etc. True when appropriate to listen fgListeningContext = False # Test speech recognition in foreground heard = '' # What the listener heard = Process is not empty listenerTimerInterval = 200 # milliseconds okToProcessListenerTimerInterrupt = True # Kludge - Do not interrupt completion routine durationOfListen = 8 # seconds maxPhraseLength = 2 # seconds sp_rec = sr.Recognizer() junkPhrase_1 = ['was perhaps', 'might have been', 'was maybe', 'seems', 'ranks'] # def isWholeNumber(s): # Borrowed from my Frequency Counter Interface n = len(s) if n == 0: return False for i in range (0, n): if (s[i] < '0' or s[i] > '9'): return False return True def isLeapYear(intYear): # return True if and only if intYear is a Gregorian leap year. if intYear % 400 == 0: return True if intYear % 100 == 0: return False if intYear % 4 == 0: return True return False def rndDate(strtYear, endYear): # return random Gregorian date between strtYear and endYear inclusive as mm/dd/yyyy format string rndYear = randint(strtYear, endYear) rndMon = randint(1, 12) daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31,30, 31, 30, 31] if isLeapYear(rndYear): daysInMonth[1] = 29 rndDay = randint(1, daysInMonth[rndMon-1]) sYear = str(rndYear).zfill(4) sMon = str(rndMon).zfill(2) sDay = str(rndDay).zfill(2) return sMon + '/' + sDay + '/' + sYear def dateToSpeakFormat(sDate): # Without enunciating leading zeros global monthName mon = monthName[int(sDate[0:2])-1] day = str(int(sDate[3:5])) year = str(int(sDate[6:])) # For year 2000 (pronounced 'two thousand'), etc. y12 = str(int(sDate[6:8])) if int(sDate[8:]) > 0: y34 = sDate[8:] # Keep 'o' as in 'oh 1 or oh 2' for 01 02, etc. else: y34 = 'hundred' if int(year) % 1000 == 0: return mon + ', ' + day + ', ' + year else: # Readability return mon + ', ' + day + ', ' + y12 + ' ' + y34 def dow(sDate): # Return day-of-week number corresponding to sDate in format mm/dd/yyyy anchorDay = [2, 0, 5, 3] iYear = int(sDate[6:]) iYY = int(sDate[8:]) iDay = int(sDate[3:5]) iMon = int(sDate[0:2]) sum = int(iYY / 12) rem = iYY % 12 sum += rem sum += int(rem / 4) sum += anchorDay[int((iYear % 400) / 100) % 4] sum += iDay mDay = magicDay[iMon-1] if isLeapYear(iYear): if iMon == 1 or iMon == 2: mDay += 1 while sum < mDay: sum += 7 return (sum - mDay) % 7 def isZeroYear(sDate): # True if and only if year carries forward 0 to month/day part # Identical to first part of dow(sDate) anchorDay = [2, 0, 5, 3] iYear = int(sDate[6:]) iYY = int(sDate[8:]) iDay = int(sDate[3:5]) iMon = int(sDate[0:2]) sum = int(iYY / 12) rem = iYY % 12 sum += rem sum += int(rem / 4) sum += anchorDay[int((iYear % 400) / 100) % 4] return sum == 0 def isHard(sDate): # 'Hard' = Jan or Feb of leap year (for double-check mark) # sDate as mm/dd/yyyy year = int(sDate[6:]) mon = int(sDate[0:2]) return mon < 3 and isLeapYear(year) def hardnessScore(sDate): # Subjective difficulty (range to be determined). # Subject to revision based on empirical evaluation. Currently not a very good measure # sDate as mm/dd/yyyy anchorDay = [2, 0, 5, 3] year = int(sDate[6:]) yy = int(sDate[8:]) mon = int(sDate[0:2]) yymod12 = yy % 12 sum = 0 # Additions if yy > 12: sum += .5 if yy / 12 >= 3: sum += 1 if yy / 12 >= 7: sum += 1 if yymod12 > 0: sum += 1 if yymod12 > 3: sum += .5 if yymod12 > 8: # Subjectively (rem == 8) is easy sum += .5 if not (mon == 4 or mon == 6 or mon == 8 or mon == 10 or mon == 12): sum += 1 if mon == 1 or mon == 2: sum += 1 if isLeapYear(year): sum += 1 # Subtractions if yy > 95: sum -= 1 if yymod12 == 0: sum -= 1 if anchorDay[int((year % 400) / 100) % 4] == 0: sum -= 1 if isZeroYear(sDate): sum -= .5 return sum + 1.5 def difficultyCategory(): global difficulty if difficulty < 0: return 0 # Not recorded if difficulty <= 3.5: return 1 if difficulty <= 5.5: return 2 return 3 def speakIt(dateText): language = 'en' myobj = gTTS(text=dateText, lang=language, slow=False) myobj.save("scratch.mp3") # os.system("start welcome.mp3") sound = AudioSegment.from_mp3("scratch.mp3") sound.export("scratch.wav", format = "wav") wave_object = sa.WaveObject.from_wave_file('scratch.wav') os.remove("scratch.mp3") play_object = wave_object.play() play_object.wait_done() os.remove("scratch.wav") return def srCleanup(txt): # Extensible fix for flawed speech recognition txt = txt.split('.')[0].lower() # .lower part is redundant txt = re.sub('[^a-zA-Z]','',txt) if txt == 'thereisday': txt = 'thursday' if txt == 'cheersday': txt = 'tuesday' return txt def negFeedbackStr(dayName): negFeedback = list() # Another extensible list # negFeedback.append('Sorry, ' + dayName + ' is not it.') negFeedback.append('Oops, ' + dayName + ' is not correct.') negFeedback.append('Sorry, your answer ' + dayName + ' does not compute.') negFeedback.append('Good heavens no. It is not ' + dayName + '.') negFeedback.append(dayName + '? Seriously? Give me a break.') negFeedback.append("Surely you're joking! " + dayName + ' is just plain wrong.') negFeedback.append('In your dreams! No, ' + dayName + ' is not correct.') negFeedback.append('How in heck did you get ' + dayName + '? That makes no sense.') return negFeedback[randint(0, len(negFeedback)-1)] # # Form implementation generated from reading ui file 'SpeakDate.ui' # # Created by: PyQt5 UI code generator 5.15.11 class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") MainWindow.resize(423, 262) self.centralwidget = QtWidgets.QWidget(MainWindow) self.centralwidget.setObjectName("centralwidget") self.endingYearLabel = QtWidgets.QLabel(self.centralwidget) self.endingYearLabel.setGeometry(QtCore.QRect(170, 30, 71, 16)) self.endingYearLabel.setObjectName("endingYearLabel") self.startingYearLabel = QtWidgets.QLabel(self.centralwidget) self.startingYearLabel.setGeometry(QtCore.QRect(20, 30, 71, 16)) self.startingYearLabel.setObjectName("startingYearLabel") self.endingYearLineEdit = QtWidgets.QLineEdit(self.centralwidget) self.endingYearLineEdit.setGeometry(QtCore.QRect(240, 20, 61, 31)) font = QtGui.QFont() font.setPointSize(14) self.endingYearLineEdit.setFont(font) self.endingYearLineEdit.setPlaceholderText("") self.endingYearLineEdit.setObjectName("endingYearLineEdit") self.startingYearLineEdit = QtWidgets.QLineEdit(self.centralwidget) self.startingYearLineEdit.setGeometry(QtCore.QRect(90, 20, 61, 31)) font = QtGui.QFont() font.setPointSize(14) self.startingYearLineEdit.setFont(font) self.startingYearLineEdit.setPlaceholderText("") self.startingYearLineEdit.setObjectName("startingYearLineEdit") self.speakRandomDatePushButton = QtWidgets.QPushButton(self.centralwidget) self.speakRandomDatePushButton.setGeometry(QtCore.QRect(20, 70, 131, 23)) self.speakRandomDatePushButton.setObjectName("speakRandomDatePushButton") self.speakRandomDatePushButton.clicked.connect(self.speakRandomDateBtnClicked) self.dayNameLineEdit = QtWidgets.QLineEdit(self.centralwidget) self.dayNameLineEdit.setGeometry(QtCore.QRect(170, 70, 141, 20)) font = QtGui.QFont() font.setPointSize(12) self.dayNameLineEdit.setFont(font) self.dayNameLineEdit.setText("") self.dayNameLineEdit.setObjectName("dayNameLineEdit") self.repeatDatePushButton = QtWidgets.QPushButton(self.centralwidget) self.repeatDatePushButton.setGeometry(QtCore.QRect(330, 70, 75, 23)) self.repeatDatePushButton.setObjectName("repeatDatePushButton") self.repeatDatePushButton.clicked.connect(self.repeatDateBtnClicked) self.feedbackTextEdit = QtWidgets.QTextEdit(self.centralwidget) self.feedbackTextEdit.setGeometry(QtCore.QRect(20, 130, 384, 71)) self.feedbackTextEdit.setReadOnly(True) self.feedbackTextEdit.setObjectName("feedbackTextEdit") self.earEmojiLabel = QtWidgets.QLabel(self.centralwidget) self.earEmojiLabel.setGeometry(QtCore.QRect(350, 20, 25, 30)) font = QtGui.QFont() font.setPointSize(16) self.earEmojiLabel.setFont(font) self.earEmojiLabel.setObjectName("earEmojiLabel") MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(MainWindow) self.menubar.setGeometry(QtCore.QRect(0, 0, 423, 21)) self.menubar.setObjectName("menubar") MainWindow.setMenuBar(self.menubar) self.statusbar = QtWidgets.QStatusBar(MainWindow) self.statusbar.setObjectName("statusbar") MainWindow.setStatusBar(self.statusbar) # self.retranslateUi(MainWindow) QtCore.QMetaObject.connectSlotsByName(MainWindow) # LM self.answerCheckingTimer = Qt.QTimer() self.answerCheckingTimer.timeout.connect(self.serviceAnalysisTimer) self.answerCheckingTimer.start(answerCheckingTimerInterval) self.ListenerTimer = Qt.QTimer() self.ListenerTimer.timeout.connect(self.serviceListenerTimer) self.ListenerTimer.start(listenerTimerInterval) def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate MainWindow.setWindowTitle(_translate("MainWindow", "Date to day-name practice")) self.endingYearLabel.setText(_translate("MainWindow", "Ending year:")) self.startingYearLabel.setText(_translate("MainWindow", "Starting year:")) self.endingYearLineEdit.setText(_translate("MainWindow", "2199")) self.startingYearLineEdit.setText(_translate("MainWindow", "1800")) self.speakRandomDatePushButton.setText(_translate("MainWindow", "Speak a random date")) self.dayNameLineEdit.setPlaceholderText(_translate("MainWindow", "Enter day of week")) self.repeatDatePushButton.setText(_translate("MainWindow", "Repeat date")) self.earEmojiLabel.setText(_translate("MainWindow", "👂")) self.earEmojiLabel.setVisible(False) random.seed(time.time()) # Added 7/17/2026 def speakRandomDateBtnClicked(self): global currentDate, correctDayNumber, difficulty, scored, spCurrentDate, startSolve global listeningContext, listening, fgListeningContext, wrongAnswer self.dayNameLineEdit.clear() self.dayNameLineEdit.repaint() listeningContext = False listening = False self.feedbackTextEdit.clear() self.dayNameLineEdit.setFocus(True) self.dayNameLineEdit.clear() self.dayNameLineEdit.repaint() self.statusbar.clearMessage() self.statusbar.repaint() wrongAnswer.clear() sYr = self.startingYearLineEdit.text() eYr = self.endingYearLineEdit.text() if isWholeNumber(sYr) and isWholeNumber(eYr): iYr = int(sYr) jYr = int(eYr) else: self.statusbar.showMessage('Start and end must be common era whole number years.') return if iYr > jYr: self.statusbar.showMessage('Starting year cannot be greater than ending year.') return heard = '' currentDate = rndDate(iYr, jYr) correctDayNumber = dow(currentDate) spCurrentDate = dateToSpeakFormat(currentDate) difficulty = hardnessScore(currentDate) # print(difficulty) # Debug (Used to make frequency distribution) speakIt(spCurrentDate) scored = False startSolve = datetime.now() listeningContext = True # Under development # fgListeningContext = True # self.listenInForeground() return def repeatDateBtnClicked(self): global spCurrentDate, listeningContext, listening, wrongAnswer listeningContext = False listening = False self.dayNameLineEdit.setText('') # Allow SR listener to restart after wrong answer self.dayNameLineEdit.repaint() self.statusbar.clearMessage() self.statusbar.repaint() heard = '' if len(spCurrentDate) > 0: speakIt(spCurrentDate) self.dayNameLineEdit.setFocus(True) wrongAnswer.clear() listeningContext = True # Under development return def completeAnalysis(self): pass def checkAnswer(self): global correctDayNumber, lowercaseDays, scored global positiveClip1, positiveClip2 global difficulty, difficultCategory, listeningContext, heard global wrongAnswer s = self.dayNameLineEdit.text().lower() if len(s) == 0: s = heard heard = '' answerDay = s # Preserve raw written or spoken response c1 = positiveClip1[randint(0, len(positiveClip1)-1)] c2 = positiveClip2[randint(0, len(positiveClip2)-1)] msg = c1 + '. ' + c2 + '.' # " That's correct!" # Placeholder for variable message if len(s) == 0 or correctDayNumber < 0: return if s in lowercaseDays: s = lowercaseDays.index(s) if isWholeNumber(str(s)): if int(s) == correctDayNumber and not scored: endSolve = datetime.now() duration = (endSolve - startSolve).total_seconds() self.feedbackTextEdit.clear() # Redundant speakIt(msg) if listeningContext == False: # Listener and speech recognition contribute to time speakIt('That solve took ' + str(round(duration)) + ' seconds.') if not (difficulty < 0): self.feedbackTextEdit.append('Difficulty: ' + str(difficulty)) self.feedbackTextEdit.repaint() if difficultyCategory() == 3 and duration > 30 and False: # Disabled (annoying) speakIt(' The challenge date ' + junkPhrase_1[randint(0, len(junkPhrase_1)-1)] + ' harder than average.') self.completeAnalysis() # Populate the feedbackTextEdit widget scored = True listeningContext = False elif not scored and not (int(s) in wrongAnswer): # s is a day-of-week, but not the correct one. # It is not a partial day or nonsense. # Therefore it is a wrong answer. wrongAnswer.append(int(s)) speakIt(negFeedbackStr(answerDay)) self.statusbar.showMessage("Click 'Repeat Date' button to retry.") self.statusbar.repaint() pass return def listenInForeground(self): # global fgListeningContext, heard, sp_rec, durationOfListen, maxPhraseLength, sr while fgListeningContext == True: self.earEmojiLabel.setVisible(True) self.earEmojiLabel.repaint() heard = '' with sr.Microphone() as source: sp_rec.adjust_for_ambient_noise(source) audio = sp_rec.listen(source, durationOfListen, maxPhraseLength) try: heard = sp_rec.recognize_whisper(audio, language="english") except sr.UnknownValueError: # print("Could not understand audio.") # Debug pass except Exception as e: # print (e) self.earEmojiLabel.setVisible(False) return # self.earEmojiLabel.setVisible(False) if len(heard): fgListeningContext = False # print(heard) # Debug (Does not require GUI update) self.dayNameLineEdit.setText(heard) self.dayNameLineEdit.repaint() checkAnswer() # Try return def processListenerTimerInterrupt(self): # Listen in background (Interrupt driven) global listening, listeningContext, heard, sp_rec, durationOfListen, maxPhraseLength if listeningContext and not listening and not len(self.dayNameLineEdit.text()): listening = True; heard = '' try: with sr.Microphone() as source: sp_rec.adjust_for_ambient_noise(source) except: pass self.earEmojiLabel.setVisible(True) self.earEmojiLabel.repaint() try: with sr.Microphone() as source: audio = sp_rec.listen(source, durationOfListen, maxPhraseLength) except sr.WaitTimeoutError: listening = False self.earEmojiLabel.setVisible(False) return try: heard = sp_rec.recognize_whisper(audio, language="english") except: listening = False self.earEmojiLabel.setVisible(False) return # if not len(heard): listening = False self.earEmojiLabel.setVisible(False) return # Fall through on none-empty text heard = srCleanup(heard) self.dayNameLineEdit.setText(heard) self.dayNameLineEdit.repaint() self.earEmojiLabel.setVisible(False) return def serviceAnalysisTimer(self): global okToCheckAnswer if okToCheckAnswer: # Ignore interrupt while checking in progress okToCheckAnswer = False self.checkAnswer() # To do: Wrap in Try/Except okToCheckAnswer = True return def serviceListenerTimer(self): # To enable ListenerTimer interrupt, uncomment start in setupUi global okToProcessListenerTimerInterrupt if okToProcessListenerTimerInterrupt: okToProcessListenerTimerInterrupt = False self.processListenerTimerInterrupt() # To do: Wrap in Try/Except okToProcessListenerTimerInterrupt = True return if __name__ == "__main__": import sys app = QtWidgets.QApplication(sys.argv) MainWindow = QtWidgets.QMainWindow() ui = Ui_MainWindow() ui.setupUi(MainWindow) MainWindow.show() sys.exit(app.exec_())