feat: a deck up to sixty slides is generated, reviewed and modified
Some checks failed
Forgejo Docker Build / Root app tests (push) Successful in 50s
Forgejo Docker Build / Build Docker image (push) Successful in 7s
Forgejo Docker Build / End-to-end (browser) (push) Failing after 8s

Twenty was the review ceiling while the form allowed thirty, so a 21-30 slide
deck was written, stored and then silently never reviewed — and modifying one
above twenty was shown only its first twenty pages while the prompt claimed
one image per slide. The three numbers are the schema's own limit now, so a
deck that can be asked for can also be stored, reviewed and edited: the form's
max, both generation clamps and MAX_SLIDES are all 60.

Measured at that size before this change: 45 pages cost 28,796 prompt tokens
and 2.2s to review, a 46-page deck with a fault on page 24 was found and
re-laid correctly, and a 44-slide deck used ~5,900 output tokens of the
16,000 ceiling.
This commit is contained in:
Daniel 2026-09-16 17:21:03 +02:00
parent b683356458
commit 1a491d7367
4 changed files with 26 additions and 4 deletions

View file

@ -23,7 +23,7 @@
</select> </select>
<label style="font-size:12px;color:var(--g600);display:flex;align-items:center;gap:6px;"> <label style="font-size:12px;color:var(--g600);display:flex;align-items:center;gap:6px;">
Slides Slides
<input id="mr-slide-count" type="number" min="3" max="30" value="8" style="width:70px;font-size:13px;padding:5px 8px;border:1px solid var(--g300);border-radius:6px;"> <input id="mr-slide-count" type="number" min="3" max="60" value="8" style="width:70px;font-size:13px;padding:5px 8px;border:1px solid var(--g300);border-radius:6px;">
</label> </label>
<label style="font-size:12px;color:var(--g600);display:flex;align-items:center;gap:6px;" id="mr-word-wrap" hidden> <label style="font-size:12px;color:var(--g600);display:flex;align-items:center;gap:6px;" id="mr-word-wrap" hidden>
Words Words

View file

@ -366,7 +366,7 @@ async function generateResource(userId, body) {
wantsImages: wantsImages, deckMode: deckMode, wantsImages: wantsImages, deckMode: deckMode,
format: deckMode ? deckFormats.formatId(body.format) : '', format: deckMode ? deckFormats.formatId(body.format) : '',
figureCount: wantsImages ? (resourceImages.requestedCount(refinement) || 0) : 0, figureCount: wantsImages ? (resourceImages.requestedCount(refinement) || 0) : 0,
slideCount: clampInt(body.slideCount, 3, 30, 8), slideCount: clampInt(body.slideCount, 3, 60, 8),
wordCount: clampInt(body.wordCount, 200, 3000, 800) wordCount: clampInt(body.wordCount, 200, 3000, 800)
}); });
@ -487,7 +487,7 @@ async function generateResource(userId, body) {
literature: sources.literature, webFindings: sources.webFindings, literature: sources.literature, webFindings: sources.webFindings,
searchedAndFoundNothing: sources.searchedAndFoundNothing, searchedAndFoundNothing: sources.searchedAndFoundNothing,
wantsImages: false, deckMode: false, wantsImages: false, deckMode: false,
slideCount: clampInt(body.slideCount, 3, 30, 8), slideCount: clampInt(body.slideCount, 3, 60, 8),
wordCount: clampInt(body.wordCount, 200, 3000, 800) wordCount: clampInt(body.wordCount, 200, 3000, 800)
}); });
ai = await callAI([{ role: 'user', content: plain }], options); ai = await callAI([{ role: 'user', content: plain }], options);

View file

@ -23,7 +23,7 @@ var os = require('os');
var pathMod = require('path'); var pathMod = require('path');
var { execFile } = require('child_process'); var { execFile } = require('child_process');
var MAX_SLIDES = 20; // a review pass is one image per slide var MAX_SLIDES = 60; // a review pass is one image per slide
var RENDER_DPI = 70; // legible to a model, small enough to send var RENDER_DPI = 70; // legible to a model, small enough to send
var CONVERT_TIMEOUT = 60000; var CONVERT_TIMEOUT = 60000;
// The reply restates the whole deck, so it needs room for one. // The reply restates the whole deck, so it needs room for one.

View file

@ -137,6 +137,28 @@ test('nothing about the review can fail a generation', async () => {
assert.match(long.reason, /too long/); assert.match(long.reason, /too long/);
}); });
test('the three caps agree, so a deck that can be generated can also be reviewed and modified', async () => {
// 20 used to be the review ceiling while the form allowed 30: a 21-30 slide
// deck was generated, stored and then silently never reviewed, and a
// modification above 20 was shown only the first 20 pages while the prompt
// claimed one image per slide. All three numbers are the schema's own limit
// now, which is what stops a deck falling between them.
assert.equal(review.MAX_SLIDES, 60, 'the review ceiling');
const route = read('src/routes/myResources.js');
assert.equal((route.match(/clampInt\(body\.slideCount, 3, 60, 8\)/g) || []).length, 2, 'both generation paths');
assert.match(read('public/components/my-resources.html'), /id="mr-slide-count"[^>]*max="60"/, 'the form');
assert.match(read('src/utils/deckSchema.js'), /slice\(0, 60\)/, 'and what the schema will store');
// The gate really moved: 60 slides is no longer "too long", 61 still is.
const sixty = await review.review({ slides: new Array(60).fill({ type: 'bullets', heading: 'x', bullets: [{ text: 'y' }] }) },
{ model: 'some-model', gotenberg: 'http://127.0.0.1:1', mime: 'application/x', pptx: Buffer.from(''), extractJson: () => null });
assert.notEqual(sixty.reason, 'deck too long to review', '60 slides is reviewed, not skipped');
assert.equal(sixty.reviewed, false, 'the renderer failed on the test\u2019s bad url, which is the point: it got that far');
const sixtyOne = await review.review({ slides: new Array(61).fill({ type: 'bullets', heading: 'x' }) }, { model: 'some-model' });
assert.equal(sixtyOne.reason, 'deck too long to review');
});
test('the reviewer is admin-chosen, off by default, and runs once per change', () => { test('the reviewer is admin-chosen, off by default, and runs once per change', () => {
const route = read('src/routes/myResources.js'); const route = read('src/routes/myResources.js');
assert.match(route, /db\.getSetting\('my_resources\.review_model', ''\)/); assert.match(route, /db\.getSetting\('my_resources\.review_model', ''\)/);