Releases: dta121/moodle-local_aireader
AI Reader 1.3.1
AI Reader 1.3.1 fixes the Moodle completion setup for listening-based completion on Page and Book activities.
- Saves AI Reader listening completion as automatic Moodle completion while clearing Moodle's native view-completion rule.
- Prevents viewing a Page or Book from satisfying the requirement before the configured listening percentage is reached.
- Allows teachers to leave Moodle's native completion option at None or choose Add requirements while enabling the AI Reader listening rule; AI Reader normalizes the stored completion mode on save.
Moodle Plugin CI passed on main: https://github.com/dta121/moodle-local_aireader/actions/runs/26969266262
AI Reader 1.3.0
AI Reader 1.3.0 Release Notes
Released: 2026-06-04
AI Reader 1.3.0 focuses on playback presentation, offline-friendly downloads,
and the first version of listening-based activity completion.
Highlights
- Admins can choose between the full player and compact banner, pill,
accordion, or inline designs, all using the same playback controls once
expanded. - A new accent colour setting themes the player controls and in-page narration
highlights. - Compact players can auto-play on expand when the browser allows it.
- Learners can download narration MP3s when downloads are enabled. Downloaded
files use human-readable filenames and remain gated by the same activity
visibility and listening permissions as streamed playback. - Teachers can require learners to listen to a configured percentage of an AI
Reader narration before Moodle marks a Page or Book activity complete. - Listening completion uses distinct played ranges from the embedded Moodle
player, so scrubbing ahead does not satisfy the rule. - Downloaded or otherwise offline MP3 playback does not count toward listening
completion in this release. Syncable offline listening is retained as a
future enhancement. - Generated MP3s now include ID3 metadata for easier recognition in local music
players.
Upgrade Notes
- Site administrators must enable Allow listening-based activity completion
before teachers see the per-activity completion fields. - Moodle activity completion must also be enabled on the target Page or Book
activity for AI Reader to mark it complete. - The upgrade adds
local_aireader_listenfor per-learner listened ranges and
local_aireader_completionfor per-activity completion settings. - The plugin now stores listened ranges as privacy-covered user data and
includes them in Moodle Privacy API export/delete flows.
v1.2.1 — Karaoke highlighting accuracy
A focused fix release for the karaoke-style in-place highlighting. A
number of sentences were narrated but never lit up on the page. The
highlighter matched each Whisper segment as a single contiguous run inside
one element and wrapped it with range.surroundContents(), which silently
dropped three classes of segment.
Fixed
- Segments that begin with text not in the page body. The narration
prepends the page title, and Whisper glues it onto the first body sentence;
the title is rendered as the page heading (excluded chrome), so the combined
text was never found — e.g. "Read this article and consider the following
questions" stayed unhighlighted. A suffix fallback now drops leading
words and matches the longest remaining run. - Segments that span an element boundary (a heading into a paragraph, one
list item into the next, an inline<em>). These were found but
surroundContentsthrew on the cross-element range and dropped them — the
bulk of the misses. Matched runs are now wrapped as one<mark>per text
node, all sharing the segment index and highlighting together. - Sentences that appear verbatim more than once (a body line restated in
the summary). The search always resolved to the first copy, so the
restatement never highlighted and the body copy was re-marked. A forward
search cursor now resolves each segment to the correct occurrence.
Together these lift in-place highlight coverage substantially on
mixed-structure pages (headings, lists, summaries, callouts).
Notes
- JavaScript-only change;
version.php-> 1.2.1 rolls the JS revision so
browsers fetch the rebuilt bundle. Empty upgrade savepoint, no schema change. - CI-verified green on Moodle 4.5 / 5.0 / 5.1 / 5.2 x PHP 8.1-8.4 x
PostgreSQL / MariaDB.
Full changelog
See CHANGELOG.md. Compare: v1.2.0...v1.2.1
v1.2.0 — Configurable dated pricing + cost by course
Added
Admin-editable, date-effective TTS pricing
A new TTS pricing setting (Site admin → Plugins → AI Reader) replaces the hard-coded cost rates. Enter one model per line as model, rate, date:
- rate = USD per 1,000,000 narration characters.
- date (optional,
YYYY-MM-DD) = effective-from date. Each asset is priced at the rate in force when it was generated, so raising a price only affects future audio — historical cost records stay accurate. To change a price going forward, add a new dated line; don't edit old ones. *works as a catch-all model.
Defaults match the previous built-in rates, so nothing changes until an admin edits them. (There is no OpenAI pricing API to pull from, so rates are maintained here, not fetched live.)
Cost-by-course report
A new page under Site administration → Reports → AI Reader cost by course lists every course that generated narration, its audio count, and estimated total spend — ordered by spend, with a grand total. Linked from the audio log report.
Changed
- The audio log's per-asset cost and summary total now use the configured, date-effective rates.
Full changelog: v1.1.0...v1.2.0
v1.1.0 — Audio generation log report
Added — Audio generation log report
A new page under Site administration → Reports → AI Reader audio log that lists every narration asset:
- Context: the course and the activity (page, or book + chapter) the audio was generated for.
- Details: language, model and voice, status, when it was generated, and the mp3 file size.
- Estimated cost per asset, derived from the narration character count and the model used.
- Failed generations are listed with their failure reason (e.g. "Narration text exceeds max (58501 > 50000 chars)").
The table is sortable, paged, filterable by status, and downloadable (CSV, Excel, etc.). A summary header shows counts by status and the estimated total spend.
Notes
- Gated by a new
local/aireader:viewlogcapability (granted to managers by default), so managers can view it without full site-config access. - Cost (
cost_calculator): tts-1 ($15) and tts-1-hd ($30) use OpenAI's published per-million-character rates; gpt-4o-mini-tts uses a blended $10/million-character estimate. Translation and Whisper alignment are billed separately and excluded. Figures are budgeting estimates, not an invoice. - A new nullable
inputcharscolumn onlocal_aireader_asset(captured at generation) drives the estimate. Audio generated before this release has no character count and shows an unknown cost ("—") until regenerated.
Full changelog: v1.0.5...v1.1.0
v1.0.5 — Moodle Plugin CI fixes
CI
Cleared the Moodle Plugin CI failures introduced during the 1.0.2–1.0.4 work. No functional change.
- phpcs (
--max-warnings 0): wrapped an over-length CJK regex line inopenai_client, capitalized the lead word of three inline comments, and dropped the unnecessaryMOODLE_INTERNALguard from the two class-only backup/restore files (moodle.Files.MoodleInternalNotNeeded). - Grunt (
--max-lint-warnings 0): rebuiltamd/build/player.min.jsand its source map fromamd/src/player.js, which had gained the GPL boilerplate header in 1.0.2 without a corresponding build regeneration. Rebuilt with Moodle's grunt (node 22 /lts/jod) and verified byte-identical via SHA-256; the minified bundle now carries the license banner.
Green across the full matrix: PHP 8.1–8.4 × Moodle 4.5/5.0/5.1/5.2 × MariaDB/PostgreSQL.
Full changelog: v1.0.4...v1.0.5
v1.0.4 — token-aware TTS chunking
Fixed
- TTS failed with Input of N tokens is over the maximum input limit of 2000 tokens on
gpt-4o-mini-tts, especially for translated (CJK) narration.chunk_text()split purely on character count (3800), which suits thetts-1family's 4096-character limit but ignores the newer model's 2000-token cap — 3800 dense CJK characters are ~3000 tokens.- Chunking is now token-aware: a new
estimate_tokens()heuristic (wide/CJK characters ~1 token each, other text ~4 chars/token, rounded up as a safe ceiling) plus an 1800-token default cap applied alongside the character cap. - Sentence splitting now recognises CJK terminators (
。.!?), which carry no trailing whitespace — so unspaced Japanese/Chinese narration splits at real sentence boundaries instead of falling through to a single oversized hard cut. - Added unit tests for the token cap and the estimator.
- Chunking is now token-aware: a new
Full changelog: v1.0.3...v1.0.4
v1.0.3 — load filelib.php (generate_audio fatal)
Fixed
generate_audiostill fatalled after 1.0.2 with Call to undefined functionfile_rewrite_pluginfile_urls(). That function is defined inlib/filelib.php, which Moodle does not auto-load in the cron / adhoc-task context the extractor runs in, so the global was genuinely undefined there — the 1.0.2 namespace qualification only changed the error text, not the outcome.content_extractor::extract()nowrequire_onces$CFG->libdir/filelib.phpbefore use. Verified on the dev host that this makes the function available (html_to_textandget_coursemodule_from_idwere already loaded). The 1.0.2 namespace qualifications are retained.
Full changelog: v1.0.2...v1.0.3
v1.0.2 — generate_audio fatal fix + Backup/Restore API
Fixed
- Fatal in the
generate_audioad hoc task. Six global Moodle functions incontent_extractor(file_rewrite_pluginfile_urls,get_coursemodule_from_id,format_string,html_to_text,get_string,get_string_manager) were called unqualified from inside thelocal_aireader\managernamespace, so PHP resolved them as undefined namespaced functions and every generation failed with Call to undefined function local_aireader\manager\file_rewrite_pluginfile_urls(). All are now fully qualified with a leading\.
Added
- Backup / Restore API support. New module-level
backup_local_plugin/restore_local_pluginclasses (backup/moodle2/) carry the per-resource narration overrides (local_aireader_override, both activity-level and per-chapter) with a backed-upmod_page/mod_bookactivity. Restore is deferred toafter_restore_module()sobook_chapterid mappings exist before chapter-level overrides are remapped; stale chapter overrides are skipped,usermodifiedis remapped when users are included, and existing(cmid, chapterid)rows are not duplicated.
Packaging
- Added the standard Moodle GPL boilerplate header to
templates/manager_offline.mustache,templates/player.mustache, andamd/src/player.js(the latter also gains a proper@module/@copyright/@licensedocblock) for plugin-directory submission.
Full changelog: v1.0.0...v1.0.2
v1.0.0 — First stable release
First stable release. Plugin is now MATURITY_STABLE.
This release lands eight findings from an internal security audit, adds a PHPUnit test suite covering the new gates, and expands CI to every supported Moodle version.
🔒 Security
| ID | Title | What it does |
|---|---|---|
| F1 | Hidden chapter gate | get_status, request_regen, and the pluginfile handler now reject hidden book chapters unless the caller has mod/book:viewhiddenchapters. Previously a learner with module-level access could narrate any chapter id. |
| F2 | Lang allowlist enforcement | The lang parameter on every external function is server-validated against the admin enabled_languages allowlist. Closes an OpenAI cost-amplification DoS. |
| F3 | Pluginfile require_login scoping |
Uses require_login($course, false, $cm) so enrolment, course visibility, and activity availability rules all apply. |
| F4 | Per-asset content cap | New max_narration_chars admin setting (default 50000) refuses runaway pages before OpenAI is called. |
| F5 | Unused capability removed | local/aireader:purge was declared but never enforced. Gone. |
| F6 | HTTPS-only outbound | New http_guard helper rejects non-HTTPS endpoints, loopback, RFC1918 private addresses, link-local, and the cloud metadata service. Curl is locked to CURLPROTO_HTTPS. |
| F7 | OpenAI error sanitization | Bearer tokens, sk-… keys, and 40+ char opaque identifiers are redacted before being stored on the asset row or written to cron logs. |
| F8 | Narrower web service return types | Segment text now PARAM_NOTAGS, status messages PARAM_TEXT (previously PARAM_RAW). |
✅ Testing
- 31 PHPUnit tests across 5 files covering the URL guard, error sanitizer, override resolution chain, translation cache LRU, asset_manager helpers, and the get_status security gates.
🔁 CI
- Matrix expanded from Moodle 4.5 only to Moodle 4.5 LTS / 5.0 / 5.1 / 5.2 on PHP 8.1 / 8.2 / 8.3 / 8.4 across both PostgreSQL 16 and MariaDB 10.11.
actions/checkoutbumped to v5 (Node.js 24) ahead of GitHub's 2026-06-02 Node 20 deprecation.Gruntstep no longer hascontinue-on-error; CSS regressions and AMD-build drift now fail loudly.- Stylelint clean.
📦 Migration notes
- No schema change. The upgrade savepoint at
2026051900registers themax_narration_charsadmin default; nothing else moves. - If you had a custom role using
local/aireader:purge, that capability is gone — but it was a no-op anyway, so role behavior is unchanged.
📜 Full changelog
See CHANGELOG.md for the complete history from 0.1.0 through 1.0.0.