playable-lessons 0.2.2 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +198 -6
- package/out/cli/index.js +415 -37
- package/out/shared/aiTaskExport.js +161 -0
- package/out/shared/caseStudyExport.js +115 -0
- package/out/shared/flashcardExport.js +161 -0
- package/out/shared/generate.js +490 -11
- package/out/shared/h5pExporter.js +286 -0
- package/out/shared/imageClient.js +227 -0
- package/out/shared/inkParser.js +187 -0
- package/out/shared/planExport.js +110 -0
- package/out/shared/project.js +12 -0
- package/out/shared/prompts.js +433 -1
- package/out/shared/quizExport.js +182 -0
- package/out/shared/storyExport.js +53 -4
- package/out/shared/summaryExport.js +144 -0
- package/out/shared/version.generated.js +7 -0
- package/package.json +7 -4
package/out/cli/index.js
CHANGED
|
@@ -10,6 +10,15 @@ const promises_1 = require("fs/promises");
|
|
|
10
10
|
const path_1 = require("path");
|
|
11
11
|
const storyExport_js_1 = require("../shared/storyExport.js");
|
|
12
12
|
const generate_js_1 = require("../shared/generate.js");
|
|
13
|
+
const imageClient_js_1 = require("../shared/imageClient.js");
|
|
14
|
+
const flashcardExport_js_1 = require("../shared/flashcardExport.js");
|
|
15
|
+
const quizExport_js_1 = require("../shared/quizExport.js");
|
|
16
|
+
const summaryExport_js_1 = require("../shared/summaryExport.js");
|
|
17
|
+
const aiTaskExport_js_1 = require("../shared/aiTaskExport.js");
|
|
18
|
+
const caseStudyExport_js_1 = require("../shared/caseStudyExport.js");
|
|
19
|
+
const planExport_js_1 = require("../shared/planExport.js");
|
|
20
|
+
const h5pExporter_js_1 = require("../shared/h5pExporter.js");
|
|
21
|
+
const version_generated_js_1 = require("../shared/version.generated.js");
|
|
13
22
|
/** Pick a provider from the environment when --provider isn't given. */
|
|
14
23
|
function resolveProviderFromEnv() {
|
|
15
24
|
if (process.env.ANTHROPIC_API_KEY)
|
|
@@ -62,6 +71,47 @@ function buildCliConfig(o) {
|
|
|
62
71
|
function slug(s) {
|
|
63
72
|
return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40) || 'story';
|
|
64
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Build an ImageProviderConfig from CLI flags + environment. Provider default:
|
|
76
|
+
* openai when OPENAI_API_KEY is set, gemini when GEMINI_API_KEY/GOOGLE_API_KEY
|
|
77
|
+
* is set, custom when --image-base-url is given.
|
|
78
|
+
*/
|
|
79
|
+
function buildCliImageConfig(o) {
|
|
80
|
+
const provider = o.imageProvider ||
|
|
81
|
+
(process.env.OPENAI_API_KEY ? 'openai' : process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY ? 'gemini' : o.imageBaseUrl ? 'custom' : undefined);
|
|
82
|
+
if (!provider) {
|
|
83
|
+
throw new Error('--images needs an image provider: set OPENAI_API_KEY or GEMINI_API_KEY, or pass --image-provider custom --image-base-url <url> --image-model <id>');
|
|
84
|
+
}
|
|
85
|
+
const config = { provider, model: o.imageModel || undefined };
|
|
86
|
+
switch (provider) {
|
|
87
|
+
case 'openai':
|
|
88
|
+
config.apiKey = process.env.OPENAI_API_KEY;
|
|
89
|
+
break;
|
|
90
|
+
case 'gemini':
|
|
91
|
+
config.apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
|
92
|
+
break;
|
|
93
|
+
case 'custom':
|
|
94
|
+
config.baseUrl = o.imageBaseUrl;
|
|
95
|
+
config.apiKey = process.env.PLAYABLE_LESSONS_IMAGE_API_KEY || process.env.OPENAI_API_KEY || '';
|
|
96
|
+
break;
|
|
97
|
+
case 'swarmui':
|
|
98
|
+
config.baseUrl = o.imageBaseUrl;
|
|
99
|
+
config.apiKey = process.env.PLAYABLE_LESSONS_IMAGE_API_KEY || process.env.SWARMUI_TOKEN || '';
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
if (!(0, imageClient_js_1.isImageProviderConfigured)(config)) {
|
|
103
|
+
throw new Error(`Image provider "${provider}" is not fully configured (missing ${provider === 'custom' || provider === 'swarmui' ? '--image-base-url' : 'API key'})`);
|
|
104
|
+
}
|
|
105
|
+
return config;
|
|
106
|
+
}
|
|
107
|
+
/** Decode a data URL into its binary payload. */
|
|
108
|
+
function dataUrlToBuffer(dataUrl) {
|
|
109
|
+
const m = dataUrl.match(/^data:([^;]+);base64,(.+)$/s);
|
|
110
|
+
if (!m)
|
|
111
|
+
throw new Error('Not a base64 data URL');
|
|
112
|
+
const ext = m[1] === 'image/jpeg' ? 'jpg' : m[1] === 'image/webp' ? 'webp' : 'png';
|
|
113
|
+
return { buffer: Buffer.from(m[2], 'base64'), ext };
|
|
114
|
+
}
|
|
65
115
|
async function readStdin() {
|
|
66
116
|
if (process.stdin.isTTY)
|
|
67
117
|
return '';
|
|
@@ -90,7 +140,7 @@ async function readStdin() {
|
|
|
90
140
|
return yargs
|
|
91
141
|
.option('input', { alias: 'i', type: 'string', demandOption: true, describe: 'Path to .ink file' })
|
|
92
142
|
.option('output', { alias: 'o', type: 'string', demandOption: true, describe: 'Output directory' })
|
|
93
|
-
.option('format', { alias: 'f', type: 'string', default: 'html', describe: 'Export format: html, ink, json' })
|
|
143
|
+
.option('format', { alias: 'f', type: 'string', default: 'html', describe: 'Export format: html, ink, json, h5p (h5p requires a story with no variables/conditionals)' })
|
|
94
144
|
.option('title', { alias: 't', type: 'string', default: 'Story', describe: 'Story title' });
|
|
95
145
|
}, async (argv) => {
|
|
96
146
|
try {
|
|
@@ -122,6 +172,13 @@ async function readStdin() {
|
|
|
122
172
|
console.log(`Exported Ink: ${outPath}`);
|
|
123
173
|
break;
|
|
124
174
|
}
|
|
175
|
+
case 'h5p': {
|
|
176
|
+
const bytes = (0, h5pExporter_js_1.exportH5P)(source, title);
|
|
177
|
+
const outPath = (0, path_1.join)(outputDir, `${baseName}.h5p`);
|
|
178
|
+
await (0, promises_1.writeFile)(outPath, Buffer.from(bytes));
|
|
179
|
+
console.log(`Exported H5P: ${outPath}`);
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
125
182
|
default:
|
|
126
183
|
console.warn(`Unknown format: ${format}`);
|
|
127
184
|
}
|
|
@@ -132,7 +189,7 @@ async function readStdin() {
|
|
|
132
189
|
process.exit(1);
|
|
133
190
|
}
|
|
134
191
|
})
|
|
135
|
-
.command('generate', 'Generate an Ink story from source material using AI', (yargs) => {
|
|
192
|
+
.command('generate', 'Generate an Ink story or flashcards from source material using AI', (yargs) => {
|
|
136
193
|
return yargs
|
|
137
194
|
.option('input', { alias: 'i', type: 'string', describe: 'Path to source material (text/markdown). Omit to use --topic or stdin.' })
|
|
138
195
|
.option('topic', { type: 'string', describe: 'A topic or brief to generate from (instead of --input)' })
|
|
@@ -142,13 +199,22 @@ async function readStdin() {
|
|
|
142
199
|
.option('base-url', { type: 'string', describe: 'Base URL for a custom OpenAI-compatible endpoint' })
|
|
143
200
|
.option('ollama-url', { type: 'string', default: 'http://localhost:11434', describe: 'Ollama base URL' })
|
|
144
201
|
.option('mode', { type: 'string', default: 'topic', describe: 'Input mode: topic | lesson | methodology | case-study | lecture-notes | scenario' })
|
|
202
|
+
.option('target', { type: 'string', default: 'story', describe: 'What to generate: story (interactive fiction) | flashcards | quiz | summary | ai-task | case-study' })
|
|
203
|
+
.option('cards', { type: 'number', default: 12, describe: 'Number of items to generate — flashcards, quiz questions, summary key points, or AI-collaboration tasks (target=flashcards|quiz|summary|ai-task)' })
|
|
204
|
+
.option('depth', { type: 'string', default: 'complete', describe: 'Case-study depth: idea | outline | complete (target=case-study)' })
|
|
145
205
|
.option('length', { alias: 'l', type: 'string', default: 'medium', describe: 'Story length: short | medium | long' })
|
|
206
|
+
.option('style', { type: 'string', default: 'stateful', describe: 'Story branching style: stateful (Ink variables + conditional text) | branching (pure tree, H5P/LMS-convertible)' })
|
|
207
|
+
.option('images', { type: 'boolean', default: false, describe: 'Generate an illustration for every scene (story target only). Images are embedded in the HTML export and written to an images/ folder.' })
|
|
208
|
+
.option('image-style', { type: 'string', default: 'cartoon', describe: 'Scene image style: cartoon | photorealistic' })
|
|
209
|
+
.option('image-provider', { type: 'string', describe: 'openai | gemini | custom | swarmui (default: inferred from OPENAI_API_KEY / GEMINI_API_KEY / --image-base-url)' })
|
|
210
|
+
.option('image-model', { type: 'string', describe: 'Image model id (defaults: gpt-image-1 | imagen-4.0-fast-generate-001; required by custom)' })
|
|
211
|
+
.option('image-base-url', { type: 'string', describe: 'Base URL for a custom OpenAI-compatible /images/generations endpoint or a SwarmUI server' })
|
|
146
212
|
.option('tone', { type: 'string', default: 'professional', describe: 'Narrative tone' })
|
|
147
213
|
.option('protagonist', { type: 'string', default: 'the reader', describe: 'Protagonist type' })
|
|
148
214
|
.option('answers', { type: 'string', describe: 'Optional answers to clarifying questions' })
|
|
149
215
|
.option('retries', { type: 'number', default: 3, describe: 'Max compile attempts, with AI-assisted fixes between them (raise for weaker local models)' })
|
|
150
|
-
.option('format', { alias: 'f', type: 'string',
|
|
151
|
-
.option('title', { alias: 't', type: 'string', describe: '
|
|
216
|
+
.option('format', { alias: 'f', type: 'string', describe: 'Output formats. story: ink|html|json|h5p (default ink,html; h5p requires --style branching). flashcards: csv|html|anki|json (default csv,html). quiz: html|txt|json (default html,json). summary: html|txt|json (default html,txt). ai-task: html|txt|json (default html,txt). case-study: html|txt|json (default html,txt).' })
|
|
217
|
+
.option('title', { alias: 't', type: 'string', describe: 'Title for the story, flashcard deck, quiz, summary, AI-task set, or case study (defaults to the input/topic name)' });
|
|
152
218
|
}, async (argv) => {
|
|
153
219
|
try {
|
|
154
220
|
// Resolve the source material: --input file, --topic string, or stdin.
|
|
@@ -174,44 +240,276 @@ async function readStdin() {
|
|
|
174
240
|
baseUrl: argv['base-url'],
|
|
175
241
|
ollamaUrl: argv['ollama-url'] || 'http://localhost:11434'
|
|
176
242
|
});
|
|
177
|
-
console.error(`Generating with ${config.provider} (${config.model})…`);
|
|
178
|
-
const result = await (0, generate_js_1.generateInk)({
|
|
179
|
-
inputMode: argv.mode,
|
|
180
|
-
inputText,
|
|
181
|
-
storyLength: argv.length,
|
|
182
|
-
protagonistType: argv.protagonist,
|
|
183
|
-
tone: argv.tone,
|
|
184
|
-
answers: argv.answers
|
|
185
|
-
}, config, { log: (m) => console.error(m), maxCompileRetries: argv.retries });
|
|
186
243
|
const outputDir = argv.output;
|
|
187
244
|
await (0, promises_1.mkdir)(outputDir, { recursive: true });
|
|
188
245
|
const title = argv.title || base;
|
|
189
|
-
const
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
246
|
+
const target = argv.target || 'story';
|
|
247
|
+
if (target !== 'story' && target !== 'flashcards' && target !== 'quiz' && target !== 'summary' && target !== 'ai-task' && target !== 'case-study') {
|
|
248
|
+
throw new Error(`Unknown --target "${target}". Use story, flashcards, quiz, summary, ai-task, or case-study.`);
|
|
249
|
+
}
|
|
250
|
+
const formatRaw = argv.format;
|
|
251
|
+
const formats = formatRaw
|
|
252
|
+
? formatRaw.split(',').map((f) => f.trim()).filter(Boolean)
|
|
253
|
+
: target === 'flashcards'
|
|
254
|
+
? ['csv', 'html']
|
|
255
|
+
: target === 'quiz'
|
|
256
|
+
? ['html', 'json']
|
|
257
|
+
: target === 'summary'
|
|
258
|
+
? ['html', 'txt']
|
|
259
|
+
: target === 'ai-task'
|
|
260
|
+
? ['html', 'txt']
|
|
261
|
+
: target === 'case-study'
|
|
262
|
+
? ['html', 'txt']
|
|
263
|
+
: ['ink', 'html'];
|
|
264
|
+
if (target === 'flashcards') {
|
|
265
|
+
console.error(`Generating flashcards with ${config.provider} (${config.model})…`);
|
|
266
|
+
const result = await (0, generate_js_1.generateFlashcards)({
|
|
267
|
+
inputMode: argv.mode,
|
|
268
|
+
inputText,
|
|
269
|
+
cardCount: argv.cards,
|
|
270
|
+
tone: argv.tone
|
|
271
|
+
}, config, { log: (m) => console.error(m) });
|
|
272
|
+
for (const format of formats) {
|
|
273
|
+
switch (format) {
|
|
274
|
+
case 'csv': {
|
|
275
|
+
const outPath = (0, path_1.join)(outputDir, `${base}.flashcards.csv`);
|
|
276
|
+
await (0, promises_1.writeFile)(outPath, (0, flashcardExport_js_1.toCSV)(result));
|
|
277
|
+
console.log(`Wrote ${outPath}`);
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
case 'html': {
|
|
281
|
+
const outPath = (0, path_1.join)(outputDir, `${base}.flashcards.html`);
|
|
282
|
+
await (0, promises_1.writeFile)(outPath, (0, flashcardExport_js_1.toStandaloneHTML)(result, title));
|
|
283
|
+
console.log(`Wrote ${outPath}`);
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
case 'anki': {
|
|
287
|
+
const outPath = (0, path_1.join)(outputDir, `${base}.flashcards.txt`);
|
|
288
|
+
await (0, promises_1.writeFile)(outPath, (0, flashcardExport_js_1.toAnkiTSV)(result));
|
|
289
|
+
console.log(`Wrote ${outPath}`);
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
case 'json': {
|
|
293
|
+
const outPath = (0, path_1.join)(outputDir, `${base}.flashcards.json`);
|
|
294
|
+
await (0, promises_1.writeFile)(outPath, JSON.stringify(result, null, 2));
|
|
295
|
+
console.log(`Wrote ${outPath}`);
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
default:
|
|
299
|
+
console.warn(`Unknown format: ${format}`);
|
|
199
300
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
else if (target === 'quiz') {
|
|
304
|
+
console.error(`Generating a quiz with ${config.provider} (${config.model})…`);
|
|
305
|
+
const result = await (0, generate_js_1.generateQuiz)({
|
|
306
|
+
inputMode: argv.mode,
|
|
307
|
+
inputText,
|
|
308
|
+
questionCount: argv.cards,
|
|
309
|
+
tone: argv.tone
|
|
310
|
+
}, config, { log: (m) => console.error(m) });
|
|
311
|
+
for (const format of formats) {
|
|
312
|
+
switch (format) {
|
|
313
|
+
case 'html': {
|
|
314
|
+
const outPath = (0, path_1.join)(outputDir, `${base}.quiz.html`);
|
|
315
|
+
await (0, promises_1.writeFile)(outPath, (0, quizExport_js_1.toStandaloneHTML)(result, title));
|
|
316
|
+
console.log(`Wrote ${outPath}`);
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
case 'json': {
|
|
320
|
+
const outPath = (0, path_1.join)(outputDir, `${base}.quiz.json`);
|
|
321
|
+
await (0, promises_1.writeFile)(outPath, JSON.stringify(result, null, 2));
|
|
322
|
+
console.log(`Wrote ${outPath}`);
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
case 'txt': {
|
|
326
|
+
const outPath = (0, path_1.join)(outputDir, `${base}.quiz.txt`);
|
|
327
|
+
await (0, promises_1.writeFile)(outPath, (0, quizExport_js_1.toPlainText)(result, title));
|
|
328
|
+
console.log(`Wrote ${outPath}`);
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
default:
|
|
332
|
+
console.warn(`Unknown format: ${format}`);
|
|
205
333
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
else if (target === 'summary') {
|
|
337
|
+
console.error(`Generating a summary with ${config.provider} (${config.model})…`);
|
|
338
|
+
const result = await (0, generate_js_1.generateSummary)({
|
|
339
|
+
inputMode: argv.mode,
|
|
340
|
+
inputText,
|
|
341
|
+
keyPointCount: argv.cards,
|
|
342
|
+
tone: argv.tone
|
|
343
|
+
}, config, { log: (m) => console.error(m) });
|
|
344
|
+
for (const format of formats) {
|
|
345
|
+
switch (format) {
|
|
346
|
+
case 'html': {
|
|
347
|
+
const outPath = (0, path_1.join)(outputDir, `${base}.summary.html`);
|
|
348
|
+
await (0, promises_1.writeFile)(outPath, (0, summaryExport_js_1.toStandaloneHTML)(result, title));
|
|
349
|
+
console.log(`Wrote ${outPath}`);
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
case 'txt': {
|
|
353
|
+
const outPath = (0, path_1.join)(outputDir, `${base}.summary.txt`);
|
|
354
|
+
await (0, promises_1.writeFile)(outPath, (0, summaryExport_js_1.toPlainText)(result, title));
|
|
355
|
+
console.log(`Wrote ${outPath}`);
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
case 'json': {
|
|
359
|
+
const outPath = (0, path_1.join)(outputDir, `${base}.summary.json`);
|
|
360
|
+
await (0, promises_1.writeFile)(outPath, JSON.stringify(result, null, 2));
|
|
361
|
+
console.log(`Wrote ${outPath}`);
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
default:
|
|
365
|
+
console.warn(`Unknown format: ${format}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
else if (target === 'ai-task') {
|
|
370
|
+
console.error(`Generating AI-collaboration tasks with ${config.provider} (${config.model})…`);
|
|
371
|
+
const result = await (0, generate_js_1.generateAiTask)({
|
|
372
|
+
inputMode: argv.mode,
|
|
373
|
+
inputText,
|
|
374
|
+
taskCount: argv.cards,
|
|
375
|
+
tone: argv.tone
|
|
376
|
+
}, config, { log: (m) => console.error(m) });
|
|
377
|
+
for (const format of formats) {
|
|
378
|
+
switch (format) {
|
|
379
|
+
case 'html': {
|
|
380
|
+
const outPath = (0, path_1.join)(outputDir, `${base}.ai-task.html`);
|
|
381
|
+
await (0, promises_1.writeFile)(outPath, (0, aiTaskExport_js_1.toStandaloneHTML)(result, title));
|
|
382
|
+
console.log(`Wrote ${outPath}`);
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
case 'txt': {
|
|
386
|
+
const outPath = (0, path_1.join)(outputDir, `${base}.ai-task.txt`);
|
|
387
|
+
await (0, promises_1.writeFile)(outPath, (0, aiTaskExport_js_1.toPlainText)(result, title));
|
|
388
|
+
console.log(`Wrote ${outPath}`);
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
case 'json': {
|
|
392
|
+
const outPath = (0, path_1.join)(outputDir, `${base}.ai-task.json`);
|
|
393
|
+
await (0, promises_1.writeFile)(outPath, JSON.stringify(result, null, 2));
|
|
394
|
+
console.log(`Wrote ${outPath}`);
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
default:
|
|
398
|
+
console.warn(`Unknown format: ${format}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
else if (target === 'case-study') {
|
|
403
|
+
console.error(`Generating a case study with ${config.provider} (${config.model})…`);
|
|
404
|
+
const result = await (0, generate_js_1.generateCaseStudy)({
|
|
405
|
+
inputMode: argv.mode,
|
|
406
|
+
inputText,
|
|
407
|
+
depth: (argv.depth || 'complete'),
|
|
408
|
+
tone: argv.tone
|
|
409
|
+
}, config, { log: (m) => console.error(m) });
|
|
410
|
+
for (const format of formats) {
|
|
411
|
+
switch (format) {
|
|
412
|
+
case 'html': {
|
|
413
|
+
const outPath = (0, path_1.join)(outputDir, `${base}.case-study.html`);
|
|
414
|
+
await (0, promises_1.writeFile)(outPath, (0, caseStudyExport_js_1.toStandaloneHTML)(result, title));
|
|
415
|
+
console.log(`Wrote ${outPath}`);
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
case 'txt': {
|
|
419
|
+
const outPath = (0, path_1.join)(outputDir, `${base}.case-study.txt`);
|
|
420
|
+
await (0, promises_1.writeFile)(outPath, (0, caseStudyExport_js_1.toPlainText)(result, title));
|
|
421
|
+
console.log(`Wrote ${outPath}`);
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
case 'json': {
|
|
425
|
+
const outPath = (0, path_1.join)(outputDir, `${base}.case-study.json`);
|
|
426
|
+
await (0, promises_1.writeFile)(outPath, JSON.stringify(result, null, 2));
|
|
427
|
+
console.log(`Wrote ${outPath}`);
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
default:
|
|
431
|
+
console.warn(`Unknown format: ${format}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
console.error(`Generating with ${config.provider} (${config.model})…`);
|
|
437
|
+
const wantImages = argv.images;
|
|
438
|
+
// Fail fast on a misconfigured image provider, before story generation.
|
|
439
|
+
const imageConfig = wantImages
|
|
440
|
+
? buildCliImageConfig({
|
|
441
|
+
imageProvider: argv['image-provider'],
|
|
442
|
+
imageModel: argv['image-model'],
|
|
443
|
+
imageBaseUrl: argv['image-base-url']
|
|
444
|
+
})
|
|
445
|
+
: undefined;
|
|
446
|
+
const result = await (0, generate_js_1.generateInk)({
|
|
447
|
+
inputMode: argv.mode,
|
|
448
|
+
inputText,
|
|
449
|
+
storyLength: argv.length,
|
|
450
|
+
branchingStyle: argv.style === 'branching' ? 'branching' : 'stateful',
|
|
451
|
+
protagonistType: argv.protagonist,
|
|
452
|
+
tone: argv.tone,
|
|
453
|
+
answers: argv.answers,
|
|
454
|
+
sceneImages: wantImages
|
|
455
|
+
}, config, { log: (m) => console.error(m), maxCompileRetries: argv.retries });
|
|
456
|
+
// Resolve # IMAGE_PROMPT: tags to real images (embedded in the HTML
|
|
457
|
+
// export and written to an images/ folder, slide-stream style).
|
|
458
|
+
let sceneImages;
|
|
459
|
+
if (wantImages && imageConfig) {
|
|
460
|
+
const imageStyle = argv['image-style'] === 'photorealistic' ? 'photorealistic' : 'cartoon';
|
|
461
|
+
console.error(`Generating scene images with ${imageConfig.provider} (${imageConfig.model || 'default model'})…`);
|
|
462
|
+
sceneImages = await (0, generate_js_1.generateSceneImages)(result.inkSource, imageConfig, {
|
|
463
|
+
style: imageStyle,
|
|
464
|
+
log: (m) => console.error(m)
|
|
465
|
+
});
|
|
466
|
+
const imagesDir = (0, path_1.join)(outputDir, 'images');
|
|
467
|
+
await (0, promises_1.mkdir)(imagesDir, { recursive: true });
|
|
468
|
+
let n = 0;
|
|
469
|
+
for (const dataUrl of Object.values(sceneImages)) {
|
|
470
|
+
n++;
|
|
471
|
+
const { buffer, ext } = dataUrlToBuffer(dataUrl);
|
|
472
|
+
await (0, promises_1.writeFile)((0, path_1.join)(imagesDir, `scene_${n}.${ext}`), buffer);
|
|
473
|
+
}
|
|
474
|
+
console.log(`Wrote ${n} scene image(s) to ${imagesDir}`);
|
|
475
|
+
}
|
|
476
|
+
const storyFormats = formats.includes('ink') ? formats : ['ink', ...formats];
|
|
477
|
+
for (const format of storyFormats) {
|
|
478
|
+
switch (format) {
|
|
479
|
+
case 'ink': {
|
|
480
|
+
const outPath = (0, path_1.join)(outputDir, `${base}.ink`);
|
|
481
|
+
await (0, promises_1.writeFile)(outPath, result.inkSource);
|
|
482
|
+
console.log(`Wrote ${outPath}`);
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
case 'json': {
|
|
486
|
+
const outPath = (0, path_1.join)(outputDir, `${base}.ink.json`);
|
|
487
|
+
await (0, promises_1.writeFile)(outPath, result.compiledJson);
|
|
488
|
+
console.log(`Wrote ${outPath}`);
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
case 'html': {
|
|
492
|
+
const html = await (0, storyExport_js_1.exportStandaloneHTML)(result.inkSource, title, sceneImages);
|
|
493
|
+
const outPath = (0, path_1.join)(outputDir, `${base}.html`);
|
|
494
|
+
await (0, promises_1.writeFile)(outPath, html);
|
|
495
|
+
console.log(`Wrote ${outPath}`);
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
case 'h5p': {
|
|
499
|
+
try {
|
|
500
|
+
const bytes = (0, h5pExporter_js_1.exportH5P)(result.inkSource, title);
|
|
501
|
+
const outPath = (0, path_1.join)(outputDir, `${base}.h5p`);
|
|
502
|
+
await (0, promises_1.writeFile)(outPath, Buffer.from(bytes));
|
|
503
|
+
console.log(`Wrote ${outPath}`);
|
|
504
|
+
}
|
|
505
|
+
catch (err) {
|
|
506
|
+
console.warn(`Skipped h5p: ${err instanceof Error ? err.message : err}`);
|
|
507
|
+
}
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
default:
|
|
511
|
+
console.warn(`Unknown format: ${format}`);
|
|
212
512
|
}
|
|
213
|
-
default:
|
|
214
|
-
console.warn(`Unknown format: ${format}`);
|
|
215
513
|
}
|
|
216
514
|
}
|
|
217
515
|
console.log('Done.');
|
|
@@ -220,8 +518,88 @@ async function readStdin() {
|
|
|
220
518
|
console.error('Generation failed:', err instanceof Error ? err.message : err);
|
|
221
519
|
process.exit(1);
|
|
222
520
|
}
|
|
521
|
+
})
|
|
522
|
+
.command('plan', 'Recommend a set of outputs for source material (optionally generate them all)', (yargs) => {
|
|
523
|
+
return yargs
|
|
524
|
+
.option('input', { alias: 'i', type: 'string', describe: 'Path to source material. Omit to use --topic or stdin.' })
|
|
525
|
+
.option('topic', { type: 'string', describe: 'A topic or brief to plan from (instead of --input)' })
|
|
526
|
+
.option('output', { alias: 'o', type: 'string', demandOption: true, describe: 'Output directory' })
|
|
527
|
+
.option('provider', { alias: 'p', type: 'string', describe: 'claude | openai | ollama | custom (default: inferred from env)' })
|
|
528
|
+
.option('model', { alias: 'm', type: 'string', describe: 'Model id' })
|
|
529
|
+
.option('base-url', { type: 'string', describe: 'Base URL for a custom OpenAI-compatible endpoint' })
|
|
530
|
+
.option('ollama-url', { type: 'string', default: 'http://localhost:11434', describe: 'Ollama base URL' })
|
|
531
|
+
.option('mode', { type: 'string', default: 'topic', describe: 'Input mode: topic | lesson | methodology | case-study | lecture-notes | scenario' })
|
|
532
|
+
.option('tone', { type: 'string', default: 'professional', describe: 'Narrative tone' })
|
|
533
|
+
.option('style', { type: 'string', default: 'stateful', describe: 'Story branching style when applying: stateful | branching' })
|
|
534
|
+
.option('apply', { type: 'boolean', default: false, describe: 'Also generate every recommended output' })
|
|
535
|
+
.option('format', { alias: 'f', type: 'string', default: 'html,json', describe: 'When applying: artifact formats html|json' });
|
|
536
|
+
}, async (argv) => {
|
|
537
|
+
try {
|
|
538
|
+
let inputText = '';
|
|
539
|
+
let base = 'plan';
|
|
540
|
+
if (argv.input) {
|
|
541
|
+
inputText = await (0, promises_1.readFile)(argv.input, 'utf-8');
|
|
542
|
+
base = (0, path_1.basename)(argv.input).replace(/\.[^.]+$/, '');
|
|
543
|
+
}
|
|
544
|
+
else if (argv.topic) {
|
|
545
|
+
inputText = argv.topic;
|
|
546
|
+
base = slug(argv.topic);
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
inputText = await readStdin();
|
|
550
|
+
}
|
|
551
|
+
if (!inputText.trim()) {
|
|
552
|
+
throw new Error('No source material — provide --input <file>, --topic "<brief>", or pipe text via stdin');
|
|
553
|
+
}
|
|
554
|
+
const config = buildCliConfig({
|
|
555
|
+
provider: argv.provider,
|
|
556
|
+
model: argv.model,
|
|
557
|
+
baseUrl: argv['base-url'],
|
|
558
|
+
ollamaUrl: argv['ollama-url'] || 'http://localhost:11434'
|
|
559
|
+
});
|
|
560
|
+
const outputDir = argv.output;
|
|
561
|
+
await (0, promises_1.mkdir)(outputDir, { recursive: true });
|
|
562
|
+
const tone = argv.tone;
|
|
563
|
+
const log = (m) => console.error(m);
|
|
564
|
+
console.error(`Planning with ${config.provider} (${config.model})…`);
|
|
565
|
+
const plan = await (0, generate_js_1.generatePlan)({ inputMode: argv.mode, inputText, tone }, config, { log });
|
|
566
|
+
await (0, promises_1.writeFile)((0, path_1.join)(outputDir, `${base}.plan.json`), JSON.stringify(plan, null, 2));
|
|
567
|
+
await (0, promises_1.writeFile)((0, path_1.join)(outputDir, `${base}.plan.txt`), (0, planExport_js_1.toPlainText)(plan));
|
|
568
|
+
await (0, promises_1.writeFile)((0, path_1.join)(outputDir, `${base}.plan.html`), (0, planExport_js_1.toStandaloneHTML)(plan, plan.title || base));
|
|
569
|
+
console.log(`Wrote plan: ${plan.recommendations.length} recommendation(s).`);
|
|
570
|
+
if (argv.apply) {
|
|
571
|
+
const formats = argv.format.split(',').map((f) => f.trim()).filter(Boolean);
|
|
572
|
+
const artifacts = await (0, generate_js_1.applyPlan)(plan, { inputMode: argv.mode, inputText, tone, branchingStyle: argv.style === 'branching' ? 'branching' : 'stateful' }, config, { log });
|
|
573
|
+
const write = async (kind, html, json) => {
|
|
574
|
+
if (formats.includes('html') && html) {
|
|
575
|
+
await (0, promises_1.writeFile)((0, path_1.join)(outputDir, `${base}.${kind}.html`), html);
|
|
576
|
+
}
|
|
577
|
+
if (formats.includes('json')) {
|
|
578
|
+
await (0, promises_1.writeFile)((0, path_1.join)(outputDir, `${base}.${kind}.json`), JSON.stringify(json, null, 2));
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
if (artifacts.story)
|
|
582
|
+
await write('story', await (0, storyExport_js_1.exportStandaloneHTML)(artifacts.story.inkSource, base), artifacts.story);
|
|
583
|
+
if (artifacts.flashcards)
|
|
584
|
+
await write('flashcards', (0, flashcardExport_js_1.toStandaloneHTML)(artifacts.flashcards, base), artifacts.flashcards);
|
|
585
|
+
if (artifacts.quiz)
|
|
586
|
+
await write('quiz', (0, quizExport_js_1.toStandaloneHTML)(artifacts.quiz, base), artifacts.quiz);
|
|
587
|
+
if (artifacts.summary)
|
|
588
|
+
await write('summary', (0, summaryExport_js_1.toStandaloneHTML)(artifacts.summary, base), artifacts.summary);
|
|
589
|
+
if (artifacts.aiTask)
|
|
590
|
+
await write('ai-task', (0, aiTaskExport_js_1.toStandaloneHTML)(artifacts.aiTask, base), artifacts.aiTask);
|
|
591
|
+
if (artifacts.caseStudy)
|
|
592
|
+
await write('case-study', (0, caseStudyExport_js_1.toStandaloneHTML)(artifacts.caseStudy, base), artifacts.caseStudy);
|
|
593
|
+
console.log('Applied: generated every recommended output.');
|
|
594
|
+
}
|
|
595
|
+
console.log('Done.');
|
|
596
|
+
}
|
|
597
|
+
catch (err) {
|
|
598
|
+
console.error('Planning failed:', err instanceof Error ? err.message : err);
|
|
599
|
+
process.exit(1);
|
|
600
|
+
}
|
|
223
601
|
})
|
|
224
602
|
.demandCommand(1, 'You must specify a command')
|
|
225
603
|
.help()
|
|
226
|
-
.version(
|
|
604
|
+
.version(version_generated_js_1.APP_VERSION)
|
|
227
605
|
.parse();
|