type is audio/mpeg (was audio/wav for an .mp3) * - audio path is absolute (no dependence on the working directory) * - user escaped in SQL; `e107.` database name replaced by MPREFIX * - falls back to any word when nothing exists at the user's level, and * shows a message instead of crashing when the word list is empty * - previous tkLastQuestion row removed before inserting the new one */ class spelling2 { // Text-to-speech server (OpenAI-compatible /v1/audio/speech endpoint). const TTS_URL = 'http://192.168.1.252:8880/v1/audio/speech'; const TTS_TIMEOUT = 20; // seconds private $user; /** * @param string $user User to supply the question for. */ public function __construct($user) { $this->user = $user; } public function getSpellingWord() { $year = max(1, (int) ceil($this->difficulty())); $question = $this->pickWord($year); if ($question === null) { $question = $this->pickWord(13); // nothing at this level: fall back to any word } if ($question === null) { e107::getRender()->tablerender('Spelling', 'No spelling words are available yet.'); return; } $questionHtml = $this->spellingHtml($question); $db = e107::getDb(); $db->delete('tkLastQuestion', "user = '" . $db->escape($this->user) . "'"); $db->insert('tkLastQuestion', ['user' => $this->user, 'LastQuestionHtml' => base64_encode($questionHtml)]); e107::getRender()->tablerender('Spelling', $questionHtml); } /** * Pick a word at or below $year, preferring words this user has answered * least in the last month. * * @return array|null Row with `word` and `usage`, or null when none found. */ private function pickWord($year) { $db = e107::getDb(); $row = $db->retrieve( "SELECT `word`, `usage`, IFNULL(`cnt`, 0) AS Ncnt FROM (SELECT `word`, `usage` FROM `" . MPREFIX . "tk_wordList` WHERE `year` <= " . (int) $year . " AND `usage` != '') AS wList LEFT JOIN (SELECT `answer`, COUNT(`answer`) AS `cnt` FROM `" . MPREFIX . "tkReport` WHERE `answereddatestamp` > UNIX_TIMESTAMP(NOW() - INTERVAL 1 MONTH) AND `username` = '" . $db->escape($this->user) . "' AND `subject` = 'spelling' GROUP BY `answer`) AS Ans ON wList.`word` = Ans.`answer` ORDER BY IFNULL(`cnt`, 0), RAND() LIMIT 1" ); return (is_array($row) && !empty($row['word'])) ? $row : null; } /** * Build the spelling form. The expected answer is stored server-side; * the form only carries a token. * * @param array $data ['word' => .., 'usage' => ..] */ private function spellingHtml($data) { $frm = e107::getForm(); $token = TkAnswerStore::issue($this->user, ['word' => $data['word'], 'usage' => $data['usage']]); $fString = $frm->open('questionForm', 'post', '', ['autocomplete' => 'off', 'class' => 'formclass']); $fString .= $frm->label('
Audio is unavailable right now.
' . htmlspecialchars((string) $blanked) . "
\n"; } $fString .= ''; $fString .= $frm->text('a1', '', 80, ['placeholder' => 'Write answer here!', 'autocomplete' => "off", 'spellcheck' => "false"]) . "\n"; $fString .= $frm->hidden('checkerData', $token, ['id' => 'checkerData']); $fString .= $frm->hidden('subject', 'spelling', ['id' => 'subject']); $fString .= '' . "\n"; $fString .= $frm->button('Answer', 'Check answer') . "\n"; $fString .= $frm->close(); $fString .= ''; return $fString; } /** * Make sure ../../audio/