Compare cards were as tall as the body whatever they held, so two columns of four short bullets sat in cards 60% empty; a one-sentence callout sat in a card two thirds of the slide. Both now size to the text. The callout card's fixed brown ink was the worst contrast in the catalogue on Board Review's mint card; it takes the theme's ink. A question slide may carry image_prompt — a film beside the stem, drawn or left as a labelled frame like a figure. The evidence-based formats (board review, journal club, QI, abstract, debrief) gain a References part and the brief says the deck ends with it: DeepSeek skipped it when it was only suggested. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fZGJNyDvERbMgS2Uc2msP
86 lines
4.5 KiB
JavaScript
86 lines
4.5 KiB
JavaScript
// A picture the deck wanted and did not get keeps its place: an empty frame
|
|
// labelled with what belongs there, for the author to fill after downloading
|
|
// — a real radiograph, a run chart, a photograph no model should draw. It
|
|
// used to become a slide of text, which forgot that a picture was wanted.
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const deckBuild = require('../src/utils/deckBuild');
|
|
const deckSchema = require('../src/utils/deckSchema');
|
|
const docSpec = require('../src/utils/docSpec');
|
|
const read = f => fs.readFileSync(path.join(__dirname, '..', f), 'utf8');
|
|
|
|
function deck() {
|
|
return deckSchema.normalise({ title: 'T', slides: [
|
|
{ type: 'figure', heading: 'The film', bullets: ['a'], image_prompt: 'the admission chest radiograph' },
|
|
{ type: 'image', heading: 'Run chart', image_prompt: 'the run chart of hand-hygiene compliance', caption: 'Weekly' },
|
|
{ type: 'custom', heading: 'Layout', shapes: [
|
|
{ kind: 'text', x: 5, y: 20, w: 40, h: 20, text: 'beside it' },
|
|
{ kind: 'image', x: 50, y: 20, w: 45, h: 50, image_prompt: 'a fishbone diagram' }
|
|
] },
|
|
{ type: 'bullets', heading: 'Plain', bullets: ['b'] }
|
|
] });
|
|
}
|
|
|
|
test('with illustration off every requested figure stays a figure, marked as a frame to fill', async () => {
|
|
const d = deck();
|
|
const result = await deckBuild.drawFigures(d, { imageModel: '' });
|
|
assert.deepEqual(result.jobs, []);
|
|
assert.equal(d.slides[0].type, 'figure');
|
|
assert.equal(d.slides[0].placeholder, true);
|
|
assert.equal(d.slides[0].image_prompt, 'the admission chest radiograph', 'the prompt is the label');
|
|
assert.equal(d.slides[1].type, 'image');
|
|
assert.equal(d.slides[1].placeholder, true);
|
|
assert.equal(d.slides[2].shapes.length, 2, 'the empty image shape is kept, not dropped');
|
|
assert.equal(d.slides[2].shapes[1].placeholder, true);
|
|
assert.equal(d.slides[3].placeholder, undefined);
|
|
});
|
|
|
|
test('the frame survives normalisation on both slides and shapes, and only as a boolean', () => {
|
|
const d = deck();
|
|
d.slides[0].placeholder = true; d.slides[2].shapes[1].placeholder = true;
|
|
const again = deckSchema.normalise(d);
|
|
assert.equal(again.slides[0].placeholder, true);
|
|
assert.equal(again.slides[2].shapes[1].placeholder, true);
|
|
const forged = deckSchema.normalise({ slides: [{ type: 'image', image_prompt: 'x', placeholder: 'yes' }] });
|
|
assert.equal(forged.slides[0].placeholder, undefined);
|
|
});
|
|
|
|
test('the Word export says what belongs in the frame', () => {
|
|
const d = deck();
|
|
d.slides[0].placeholder = true; d.slides[2].shapes[1].placeholder = true;
|
|
const blocks = docSpec.fromDeck ? docSpec.fromDeck(d, {}) : docSpec.build(d, {});
|
|
const text = JSON.stringify(blocks);
|
|
assert.match(text, /\[Add an image here: the admission chest radiograph\]/);
|
|
assert.match(text, /\[Add an image here: a fishbone diagram\]/);
|
|
assert.doesNotMatch(text, /hand-hygiene/, 'a slide not marked as a frame gets no placeholder line');
|
|
});
|
|
|
|
test('the renderer draws the frame on figure, image and custom image shapes', () => {
|
|
const py = read('scripts/render_pptx.py');
|
|
assert.match(py, /def _placeholder\(slide, left, top, width, height, label\)/);
|
|
assert.match(py, /MSO_LINE_DASH_STYLE\.DASH/);
|
|
assert.equal((py.match(/\n\s+_placeholder\(slide, /g) || []).length, 4, 'figure, image, question and draw_image each draw it');
|
|
assert.match(py, /"Add an image here"/);
|
|
});
|
|
|
|
test('the model is told a picture it names is drawn or left as a frame, never described in prose', () => {
|
|
const brief = deckSchema.instructions(8, 0);
|
|
assert.match(brief, /keeps an empty labelled/);
|
|
assert.match(brief, /Never write an image placeholder into any text/);
|
|
});
|
|
|
|
test('a question may carry a picture beside its stem, drawn or left as a frame', async () => {
|
|
const d = deckSchema.normalise({ slides: [
|
|
{ type: 'question', heading: 'Case 2', question: 'What does the film show?', options: ['a', 'b'], answer: 'a', image_prompt: 'the admission chest radiograph' }
|
|
] });
|
|
assert.equal(d.slides[0].type, 'question', 'not converted into a figure slide');
|
|
assert.equal(d.slides[0].image_prompt, 'the admission chest radiograph');
|
|
await deckBuild.drawFigures(d, { imageModel: '' });
|
|
assert.equal(d.slides[0].placeholder, true);
|
|
const blocks = docSpec.fromDeck(d, {});
|
|
assert.match(JSON.stringify(blocks), /\[Add an image here: the admission chest radiograph\]/);
|
|
const py = read('scripts/render_pptx.py');
|
|
assert.match(py, /has_picture = bool\(path and os\.path\.exists\(path\)\) or bool\(spec\.get\("placeholder"\)\)/);
|
|
});
|