John Smith commited on 2023-98 00:07:59
Showing 2 changed files, with 120 additions and 0 deletions.
With the "v" option, Memoire can run interactive reviews thru text-to-speech, now requiring only text input and speakers rather than a screen. The user can customise the text-to-speech with the EspeakFlags config option, and can have the cards processed more precisely than multiclozes by writing a prspeak.lua in their configuration directory.
| ... | ... |
@@ -175,6 +175,27 @@ local function mismatched(s) |
| 175 | 175 |
return ("}" .. s):find("}[^{]+}") or (s .. "{"):find("{[^}]+{")
|
| 176 | 176 |
end |
| 177 | 177 |
|
| 178 |
+--[[ |
|
| 179 |
+ presents `text` thru text-to-speech, |
|
| 180 |
+ with language/voice code `lang` (`false` or `nil` for default) |
|
| 181 |
+ `text` and `lang` may also both be parallel tables |
|
| 182 |
+ implementation may be platform-specific |
|
| 183 |
+]] |
|
| 184 |
+local function speak(text, conf, lang) |
|
| 185 |
+ if type(text) == "string" and (type(lang) == "string" or type(lang) == "nil") then |
|
| 186 |
+ text = {text}
|
|
| 187 |
+ lang = {lang}
|
|
| 188 |
+ end |
|
| 189 |
+ for i, t in ipairs(text) do |
|
| 190 |
+ os.execute(string.format( |
|
| 191 |
+ "espeak %s %s %q", |
|
| 192 |
+ conf:get("EspeakFlags") or "",
|
|
| 193 |
+ lang[i] and ("-v " .. lang[i]) or "",
|
|
| 194 |
+ t |
|
| 195 |
+ )) |
|
| 196 |
+ end |
|
| 197 |
+end |
|
| 198 |
+ |
|
| 178 | 199 |
local actions = {
|
| 179 | 200 |
a = {
|
| 180 | 201 |
desc = "(a)dd card (from stdin)", |
| ... | ... |
@@ -482,6 +503,89 @@ local actions = {
|
| 482 | 503 |
return deck |
| 483 | 504 |
end, |
| 484 | 505 |
}, |
| 506 |
+ |
|
| 507 |
+ v = {
|
|
| 508 |
+ desc = "(v)ocal review session", |
|
| 509 |
+ nd = true, |
|
| 510 |
+ fn = function(config, deck, deckfname) |
|
| 511 |
+ if not isatty() then |
|
| 512 |
+ io.write("Review should be done in the terminal\n")
|
|
| 513 |
+ return nil |
|
| 514 |
+ end |
|
| 515 |
+ -- collect list of index-pairs for prompts to be reviewed this session |
|
| 516 |
+ local ctrt = review.list(deck, config, function(pr) return not pr:spt():find("IMG%[") end)
|
|
| 517 |
+ speak(string.format( |
|
| 518 |
+ "%d prompts to review; how many in this session? ", #ctrt |
|
| 519 |
+ ), config) |
|
| 520 |
+ local climt = io.read("l")
|
|
| 521 |
+ local climn = tonumber(climt) |
|
| 522 |
+ if not climn then |
|
| 523 |
+ speak("Input not a number; aborting", conf)
|
|
| 524 |
+ return nil |
|
| 525 |
+ end |
|
| 526 |
+ climn = math.min(climn, #ctrt) |
|
| 527 |
+ local lrc = 0 |
|
| 528 |
+ local editqueue = {}
|
|
| 529 |
+ local start = os.time() |
|
| 530 |
+ -- review prompts in this session's set |
|
| 531 |
+ for ind, ip in ipairs(ctrt) do |
|
| 532 |
+ if ind > climn then |
|
| 533 |
+ break |
|
| 534 |
+ end |
|
| 535 |
+ local pr = deck[ip[1]][ip[2]] |
|
| 536 |
+ -- remove braces for display |
|
| 537 |
+ local pst = pr.text:gsub("%{([^{}]+)%}", "%1")
|
|
| 538 |
+ local st = os.time() |
|
| 539 |
+ io.write(pst .. "\n" .. ind .. "/" .. climn .. "\n") |
|
| 540 |
+ local qt, ql, at, al = prspeak(pr) |
|
| 541 |
+ local heard = false |
|
| 542 |
+ while not heard do |
|
| 543 |
+ speak(qt, config, ql) |
|
| 544 |
+ if io.read("l") ~= "r" then heard = true end
|
|
| 545 |
+ end |
|
| 546 |
+ speak(at, config, al) |
|
| 547 |
+ speak("hard, good, easy?", config)
|
|
| 548 |
+ local resp = io.read("l")
|
|
| 549 |
+ local sc = succfromstr(resp) |
|
| 550 |
+ if sc == -1 then |
|
| 551 |
+ table.insert(editqueue, ip) |
|
| 552 |
+ else |
|
| 553 |
+ if sc == 0 then |
|
| 554 |
+ lrc = lrc + 1 |
|
| 555 |
+ end |
|
| 556 |
+ review.update(deck[ip[1]][ip[2]], st, sc, config) |
|
| 557 |
+ end |
|
| 558 |
+ clear() |
|
| 559 |
+ if resp:sub(#resp):lower() == "q" then |
|
| 560 |
+ climn = ind |
|
| 561 |
+ break |
|
| 562 |
+ end |
|
| 563 |
+ end |
|
| 564 |
+ if #ctrt > 0 then |
|
| 565 |
+ local endt = os.time() |
|
| 566 |
+ io.write(string.format( |
|
| 567 |
+ "Reviewed %d prompts in %d seconds\n", |
|
| 568 |
+ math.min(#ctrt, climn), |
|
| 569 |
+ endt - start |
|
| 570 |
+ )) |
|
| 571 |
+ local s |
|
| 572 |
+ if lrc == 1 then s = "" else s = "s" end |
|
| 573 |
+ io.write(string.format( |
|
| 574 |
+ "(%d lapse%s, %f seconds per card)\n", |
|
| 575 |
+ lrc, s, (endt - start) / math.min(#ctrt, climn) |
|
| 576 |
+ )) |
|
| 577 |
+ end |
|
| 578 |
+ if #editqueue > 0 then |
|
| 579 |
+ io.write(string.format("The following cards are marked for correction:\n"))
|
|
| 580 |
+ for _, ip in ipairs(editqueue) do |
|
| 581 |
+ io.write(string.format("%d:%d %s\n", ip[1], ip[2], deck[ip[1]][ip[2]]))
|
|
| 582 |
+ end |
|
| 583 |
+ end |
|
| 584 |
+ if climn > 0 then |
|
| 585 |
+ return deck |
|
| 586 |
+ end |
|
| 587 |
+ end, |
|
| 588 |
+ }, |
|
| 485 | 589 |
} |
| 486 | 590 |
|
| 487 | 591 |
return actions |
| ... | ... |
@@ -73,6 +73,22 @@ else |
| 73 | 73 |
deckfname = config.DeckFile[1] |
| 74 | 74 |
end |
| 75 | 75 |
|
| 76 |
+--[[ |
|
| 77 |
+ returns four tables: |
|
| 78 |
+ question-text, question-lang, answer-text, answer-lang |
|
| 79 |
+]] |
|
| 80 |
+defaultprspeak = function(pr) |
|
| 81 |
+ return {(pr.text:gsub("%{([^{}]+)%}", "%1"):gsub("%%A", "WHAT"))}, {false}, {"ANSWER: " .. pr.ans}, {false}
|
|
| 82 |
+end |
|
| 83 |
+prspeak = defaultprspeak |
|
| 84 |
+do |
|
| 85 |
+local fh = io.open(confdir .. "prspeak.lua") |
|
| 86 |
+if fh then |
|
| 87 |
+ fh:close() |
|
| 88 |
+ prspeak = dofile(confdir .. "prspeak.lua") |
|
| 89 |
+end |
|
| 90 |
+end |
|
| 91 |
+ |
|
| 76 | 92 |
deckfname = os.getenv("HOME") .. "/" .. deckfname
|
| 77 | 93 |
|
| 78 | 94 |
math.randomseed(os.time()) |
| 79 | 95 |