@thanh01.pmt/curriculum-kit 1.4.15 → 1.4.17

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/dist/index.cjs CHANGED
@@ -2,12 +2,13 @@
2
2
 
3
3
  var fs2 = require('fs');
4
4
  var path3 = require('path');
5
- var jsonrepair = require('jsonrepair');
6
- var zod = require('zod');
7
5
  var google = require('@ai-sdk/google');
8
6
  var openai = require('@ai-sdk/openai');
9
7
  var deepseek = require('@ai-sdk/deepseek');
8
+ var zod = require('zod');
10
9
  var ai = require('ai');
10
+ var pRetry = require('p-retry');
11
+ var jsonrepair = require('jsonrepair');
11
12
  var pLimit = require('p-limit');
12
13
  var fsPromises = require('fs/promises');
13
14
  var crypto = require('crypto');
@@ -20,6 +21,7 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
20
21
 
21
22
  var fs2__default = /*#__PURE__*/_interopDefault(fs2);
22
23
  var path3__default = /*#__PURE__*/_interopDefault(path3);
24
+ var pRetry__default = /*#__PURE__*/_interopDefault(pRetry);
23
25
  var pLimit__default = /*#__PURE__*/_interopDefault(pLimit);
24
26
  var fsPromises__default = /*#__PURE__*/_interopDefault(fsPromises);
25
27
  var crypto__default = /*#__PURE__*/_interopDefault(crypto);
@@ -889,6 +891,132 @@ var init_streamRunner = __esm({
889
891
  exports.DEFAULT_ARTIFACT_STREAM_TOTAL_MS = 48e4;
890
892
  }
891
893
  });
894
+ function getAIModel(options = {}) {
895
+ const designated = getDesignatedFallbackConfig();
896
+ const defaultProvider = process.env.DEFAULT_AI_PROVIDER || process.env.LLM_PROVIDER || detectDefaultProvider();
897
+ let provider = (options.provider || defaultProvider).toLowerCase();
898
+ if (!isProviderEnabled(provider)) {
899
+ console.warn(`[provider-factory] Provider "${provider}" is disabled via environment toggle. Falling back to designated provider "${designated.provider}".`);
900
+ provider = designated.provider;
901
+ }
902
+ let resolvedModelName = options.modelName || process.env.DEFAULT_AI_MODEL || process.env.LLM_MODEL || DEFAULT_MODELS[provider] || designated.model;
903
+ if (!isModelAllowed(resolvedModelName)) {
904
+ console.warn(`[provider-factory] Model "${resolvedModelName}" is not permitted by ALLOWED_AI_MODELS. Using designated model "${designated.model}".`);
905
+ resolvedModelName = designated.model;
906
+ }
907
+ if (provider === "nvidia") {
908
+ const apiKey = options.apiKey || process.env.NVIDIA_API_KEY || "";
909
+ const nvidia = openai.createOpenAI({
910
+ apiKey,
911
+ baseURL: options.baseURL || "https://integrate.api.nvidia.com/v1"
912
+ });
913
+ return nvidia.chat(resolvedModelName || "nvidia/nemotron-3-ultra-550b-a55b:free");
914
+ }
915
+ if (provider === "openrouter") {
916
+ const apiKey = options.apiKey || process.env.OPENROUTER_API_KEY || process.env.OPENAI_API_KEY || "";
917
+ const openrouter = openai.createOpenAI({
918
+ apiKey,
919
+ baseURL: options.baseURL || "https://openrouter.ai/api/v1"
920
+ });
921
+ return openrouter.chat(resolvedModelName || "@preset/coding-free");
922
+ }
923
+ if (provider === "alibaba" || provider === "dashscope") {
924
+ const effectiveKey = options.apiKey || process.env.ALIBABA_API_KEY || process.env.DASHSCOPE_API_KEY || process.env.SAAS_ALIBABA_API_KEY || process.env.SAAS_DASHSCOPE_API_KEY || "";
925
+ let defaultBaseURL = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
926
+ if (effectiveKey.startsWith("sk-sp-")) {
927
+ defaultBaseURL = "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1";
928
+ }
929
+ const rawUrl = options.baseURL || process.env.ALIBABA_BASE_URL || process.env.DASHSCOPE_BASE_URL || defaultBaseURL;
930
+ const cleanBaseURL = rawUrl.replace(/^["']|["']$/g, "").trim();
931
+ const alibaba = openai.createOpenAI({
932
+ apiKey: effectiveKey,
933
+ baseURL: cleanBaseURL
934
+ // Passthrough: no forced format injection — the Vercel AI SDK's generateObject
935
+ // already instructs the model to return JSON via its schema definition.
936
+ // Injecting 'Output format: JSON.' into markdown-based lesson prompts caused
937
+ // model confusion (mixed markdown + JSON output).
938
+ });
939
+ return alibaba.chat(resolvedModelName);
940
+ }
941
+ if (provider === "google") {
942
+ const apiKey = options.apiKey || process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY || "";
943
+ const google$1 = google.createGoogleGenerativeAI({ apiKey });
944
+ return google$1(resolvedModelName);
945
+ }
946
+ if (provider === "deepseek") {
947
+ const apiKey = options.apiKey || process.env.DEEPSEEK_API_KEY || process.env.SAAS_DEEPSEEK_API_KEY || "";
948
+ const deepseek$1 = deepseek.createDeepSeek({ apiKey });
949
+ return deepseek$1(resolvedModelName);
950
+ }
951
+ if (provider === "openai") {
952
+ const apiKey = options.apiKey || process.env.OPENAI_API_KEY || "";
953
+ const openai$1 = openai.createOpenAI({
954
+ apiKey,
955
+ baseURL: options.baseURL || process.env.OPENAI_BASE_URL
956
+ });
957
+ return openai$1.chat(resolvedModelName);
958
+ }
959
+ if (provider === "ollama") {
960
+ const apiKey = options.apiKey || process.env.OLLAMA_API_KEY || "ollama";
961
+ const baseURL = options.baseURL || process.env.OLLAMA_BASE_URL || process.env.OPENAI_BASE_URL || "http://127.0.0.1:11434/v1";
962
+ const ollama = openai.createOpenAI({
963
+ apiKey,
964
+ baseURL
965
+ });
966
+ return ollama.chat(resolvedModelName || "qwen2.5:latest");
967
+ }
968
+ const fallback = openai.createOpenAI({
969
+ apiKey: options.apiKey || process.env.OPENAI_API_KEY || "",
970
+ baseURL: options.baseURL || process.env.OPENAI_BASE_URL
971
+ });
972
+ return fallback.chat(resolvedModelName);
973
+ }
974
+ function detectDefaultProvider() {
975
+ if (process.env.NVIDIA_API_KEY && isProviderEnabled("nvidia")) {
976
+ return "nvidia";
977
+ }
978
+ if (process.env.OPENROUTER_API_KEY && isProviderEnabled("openrouter")) {
979
+ return "openrouter";
980
+ }
981
+ if ((process.env.ALIBABA_API_KEY || process.env.DASHSCOPE_API_KEY || process.env.SAAS_ALIBABA_API_KEY) && isProviderEnabled("alibaba")) {
982
+ return "alibaba";
983
+ }
984
+ if ((process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY) && (isProviderEnabled("gemini") || isProviderEnabled("google"))) {
985
+ return "google";
986
+ }
987
+ if ((process.env.DEEPSEEK_API_KEY || process.env.SAAS_DEEPSEEK_API_KEY) && isProviderEnabled("deepseek")) {
988
+ return "deepseek";
989
+ }
990
+ if (process.env.OPENAI_API_KEY && process.env.OPENAI_API_KEY !== "ollama" && isProviderEnabled("openai")) {
991
+ return "openai";
992
+ }
993
+ return getDesignatedFallbackConfig().provider;
994
+ }
995
+ var DEFAULT_MODELS; exports.NVIDIA_MODELS = void 0;
996
+ var init_provider_factory = __esm({
997
+ "src/ai/provider-factory.ts"() {
998
+ init_streamRunner();
999
+ DEFAULT_MODELS = {
1000
+ nvidia: "nvidia/nemotron-3-ultra-550b-a55b:free",
1001
+ openrouter: "@preset/coding-free",
1002
+ alibaba: "qwen3.7-plus",
1003
+ dashscope: "qwen3.7-plus",
1004
+ google: "gemini-1.5-flash",
1005
+ deepseek: "deepseek-chat",
1006
+ openai: "gpt-4o-mini",
1007
+ ollama: "deepseek-v4-flash:0731"
1008
+ };
1009
+ exports.NVIDIA_MODELS = {
1010
+ codingFast: "nvidia/nemotron-3.5-lightning-30b-a3b",
1011
+ reasoningUltra: "nvidia/nemotron-3-ultra-550b-a55b:free",
1012
+ superMoE: "nvidia/nemotron-3-super-120b-a12b",
1013
+ llamaNemotron: "nvidia/llama-3.1-nemotron-70b-instruct",
1014
+ translation: "nvidia/riva-translate-4b-instruct-v2",
1015
+ embedding: "nvidia/nemotron-3-embed-1b",
1016
+ visionLlama: "meta/llama-3.2-11b-vision-instruct"
1017
+ };
1018
+ }
1019
+ });
892
1020
 
893
1021
  // src/pipeline/artifactLinter.ts
894
1022
  var artifactLinter_exports = {};
@@ -1300,8 +1428,9 @@ ${p.content.slice(0, 500)}...`
1300
1428
  const countGuidance = targetSlideCount ? `The target is approximately ${targetSlideCount} slides.` : `Determine the organic, optimal number of slides based directly on the canonical LESSON plan: every instructional move, concept explanation, code walkthrough, activity phase, and quiz checkpoint must receive adequate slide coverage without artificial cramming or stretching.`;
1301
1429
  const outputGuidance = targetSlideCount ? `Output ONLY a valid JSON array of ${targetSlideCount} objects.` : `Output ONLY a valid JSON array of objects (one object per planned slide).`;
1302
1430
  return `
1303
- You are @illustrator, Chief Slide Architect for the Course Builder OS.
1304
- Your task is to analyze the canonical LESSON plan and architect an authoritative, high-fidelity **SLIDE BLUEPRINT**.
1431
+ You are @illustrator, Chief Slide Architect.
1432
+ Your task is to analyze the canonical LESSON plan ("${lessonFlow.lessonTitle}") and architect an authoritative, high-fidelity **SLIDE BLUEPRINT** (a slide-by-slide presentation deck outline for this single lesson).
1433
+ IMPORTANT: Do NOT output a course outline or lessons list. You are designing presentation SLIDES for this lesson.
1305
1434
  ${countGuidance}
1306
1435
 
1307
1436
  ### \u{1F3DB}\uFE0F PEDAGOGICAL GROUND TRUTH (FROM CANONICAL LESSON):
@@ -1466,12 +1595,52 @@ var init_slideBatchPrompt = __esm({
1466
1595
  // src/services/slideProductionWorkflow.ts
1467
1596
  var slideProductionWorkflow_exports = {};
1468
1597
  __export(slideProductionWorkflow_exports, {
1598
+ GeneratedSlideArraySchema: () => exports.GeneratedSlideArraySchema,
1599
+ GeneratedSlideSchema: () => exports.GeneratedSlideSchema,
1600
+ SlideBlueprintArraySchema: () => exports.SlideBlueprintArraySchema,
1601
+ SlideBlueprintItemSchema: () => exports.SlideBlueprintItemSchema,
1469
1602
  executeSlideProductionWorkflow: () => executeSlideProductionWorkflow
1470
1603
  });
1471
- function extractCleanJson(raw) {
1472
- const fenceMatch = raw.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
1473
- const candidate = fenceMatch ? fenceMatch[1] : raw;
1474
- return candidate.trim();
1604
+ async function inferStructured(schema, systemPrompt, userPrompt, options, label) {
1605
+ try {
1606
+ const model = getAIModel(options.modelOptions);
1607
+ const { object } = await ai.generateObject({
1608
+ model,
1609
+ schema,
1610
+ system: systemPrompt || void 0,
1611
+ prompt: userPrompt
1612
+ });
1613
+ const validated = schema.safeParse(object);
1614
+ if (validated.success) return validated.data;
1615
+ console.warn(
1616
+ `[SlideProductionWorkflow] AI SDK output failed schema validation for ${label}:`,
1617
+ validated.error?.message
1618
+ );
1619
+ } catch (aiSdkErr) {
1620
+ console.warn(`[SlideProductionWorkflow] AI SDK generateObject failed for ${label}:`, aiSdkErr?.message || aiSdkErr);
1621
+ }
1622
+ if (options.allowLegacyFallback === false) return null;
1623
+ try {
1624
+ const messages = [
1625
+ { role: "user", content: [systemPrompt, userPrompt].filter(Boolean).join("\n\n") }
1626
+ ];
1627
+ const raw = await runCurriculumAIInference(messages, options.satelliteContext || "", options.runnerOptions);
1628
+ const fenceMatch = raw.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
1629
+ const candidate = (fenceMatch ? fenceMatch[1] : raw).trim();
1630
+ let parsed;
1631
+ try {
1632
+ parsed = JSON.parse(candidate);
1633
+ } catch {
1634
+ parsed = JSON.parse(jsonrepair.jsonrepair(candidate));
1635
+ }
1636
+ const result = schema.safeParse(parsed);
1637
+ if (result.success) return result.data;
1638
+ console.warn(`[SlideProductionWorkflow] Legacy fallback output failed schema validation for ${label}:`, result.error?.message);
1639
+ return null;
1640
+ } catch (legacyErr) {
1641
+ console.warn(`[SlideProductionWorkflow] Legacy fallback failed for ${label}:`, legacyErr?.message || legacyErr);
1642
+ return null;
1643
+ }
1475
1644
  }
1476
1645
  async function executeSlideProductionWorkflow(options) {
1477
1646
  const {
@@ -1483,8 +1652,7 @@ async function executeSlideProductionWorkflow(options) {
1483
1652
  language = "Vietnamese",
1484
1653
  languageDirective = "",
1485
1654
  headingDirective = "",
1486
- satelliteContext,
1487
- runnerOptions,
1655
+ maxRetries = 3,
1488
1656
  onProgress
1489
1657
  } = options;
1490
1658
  let presentationKitSkills = null;
@@ -1500,41 +1668,44 @@ async function executeSlideProductionWorkflow(options) {
1500
1668
  const countLabel = targetSlideCount ? `${targetSlideCount} ` : "h\u1EEFu c\u01A1 ";
1501
1669
  onProgress?.("@illustrator", `[1/4] Ph\xE2n t\xEDch m\u1EA1ch b\xE0i gi\u1EA3ng LESSON & L\u1EADp D\xE0n \xFD ${countLabel}Slides...`);
1502
1670
  const lessonFlow = parseLessonFlow(lessonMarkdown);
1503
- const stylePreset = presentationKitSkills?.getStylePreset ? presentationKitSkills.getStylePreset(stylePresetId) : { name: "Blue Professional" };
1671
+ const getStylePreset = presentationKitSkills?.getStylePreset;
1672
+ const stylePreset = getStylePreset ? getStylePreset(stylePresetId) : { name: "Blue Professional" };
1504
1673
  const blueprintPrompt = buildSlideBlueprintPrompt({
1505
1674
  lessonFlow,
1506
1675
  targetSlideCount,
1507
1676
  stylePresetName: stylePreset?.name || "Blue Professional"
1508
1677
  });
1509
- const rawBlueprint = await runCurriculumAIInference(
1510
- [{ role: "user", content: blueprintPrompt }],
1511
- satelliteContext,
1512
- runnerOptions,
1513
- (chunk, type) => {
1514
- onProgress?.("@illustrator", chunk, { type: type || "content", artifactType: "SLIDE" });
1515
- }
1516
- );
1517
- let blueprintItems = [];
1518
- try {
1519
- blueprintItems = JSON.parse(extractCleanJson(rawBlueprint));
1520
- } catch {
1521
- try {
1522
- blueprintItems = JSON.parse(jsonrepair.jsonrepair(extractCleanJson(rawBlueprint)));
1523
- } catch (e) {
1524
- console.warn(`[SlideProductionWorkflow] Failed parsing blueprint JSON, constructing fallback:`, e);
1525
- const fallbackCount = targetSlideCount || Math.max(12, Math.min(24, Math.max(lessonFlow.phases.length, 4) * 4));
1526
- blueprintItems = Array.from({ length: fallbackCount }, (_, i) => ({
1527
- slideIndex: i + 1,
1528
- clusterId: Math.floor(i / 5) + 1,
1529
- clusterTitle: `Cluster ${Math.floor(i / 5) + 1}`,
1530
- lessonPhase: lessonFlow.phases[Math.min(i, lessonFlow.phases.length - 1)]?.phaseName || "Content",
1531
- layoutId: i === 0 ? "hero-cover" : i === fallbackCount - 1 ? "summary-takeaways" : "split-concept-code",
1532
- title: `Slide ${i + 1}: ${lessonTitle}`,
1533
- pedagogicalGoal: `Teach step ${i + 1} of ${lessonTitle}`,
1534
- contentFocus: ["Key point 1", "Key point 2", "Key point 3"]
1678
+ const blueprintItems = await pRetry__default.default(
1679
+ async () => {
1680
+ const items = await inferStructured(
1681
+ exports.SlideBlueprintArraySchema,
1682
+ "",
1683
+ blueprintPrompt,
1684
+ options,
1685
+ "blueprint"
1686
+ );
1687
+ if (!items || items.length === 0) {
1688
+ throw new Error("Blueprint generation returned empty or schema-invalid output");
1689
+ }
1690
+ return items.sort((a, b) => a.slideIndex - b.slideIndex).map((item, idx) => ({
1691
+ ...item,
1692
+ slideIndex: idx + 1,
1693
+ clusterId: item.clusterId || Math.floor(idx / 5) + 1,
1694
+ clusterTitle: item.clusterTitle || "Cluster",
1695
+ lessonPhase: item.lessonPhase || "Content",
1696
+ pedagogicalGoal: item.pedagogicalGoal || "",
1697
+ contentFocus: item.contentFocus || []
1535
1698
  }));
1699
+ },
1700
+ {
1701
+ retries: maxRetries - 1,
1702
+ onFailedAttempt: (err) => {
1703
+ onProgress?.("@illustrator", `\u26A0\uFE0F Blueprint attempt ${err.attemptNumber}/${maxRetries} failed \u2014 retrying...`, {
1704
+ type: "warning"
1705
+ });
1706
+ }
1536
1707
  }
1537
- }
1708
+ );
1538
1709
  const clustersMap = /* @__PURE__ */ new Map();
1539
1710
  for (const item of blueprintItems) {
1540
1711
  const cId = item.clusterId || Math.floor((item.slideIndex - 1) / 5) + 1;
@@ -1542,17 +1713,16 @@ async function executeSlideProductionWorkflow(options) {
1542
1713
  clustersMap.get(cId).push(item);
1543
1714
  }
1544
1715
  const clusters = Array.from(clustersMap.entries()).sort(([a], [b]) => a - b);
1545
- const allGeneratedSlides = [];
1546
1716
  const skillPrompt = presentationKitSkills?.getFrontendSlidesSkillPrompt ? presentationKitSkills.getFrontendSlidesSkillPrompt({ stylePresetId }) : "";
1717
+ const allGeneratedSlides = [];
1718
+ const failedClusters = [];
1547
1719
  let clusterIdx = 0;
1548
1720
  for (const [cId, clusterSlides] of clusters) {
1549
1721
  clusterIdx++;
1550
1722
  const clusterTitle = clusterSlides[0]?.clusterTitle || `Giai \u0111o\u1EA1n ${clusterIdx}`;
1551
1723
  onProgress?.("@illustrator", `[2/4] Sinh chi ti\u1EBFt C\u1EE5m ${clusterIdx}/${clusters.length}: "${clusterTitle}" (${clusterSlides.length} slides)...`);
1552
1724
  const clusterPhaseNames = new Set(clusterSlides.map((s) => s.lessonPhase.toLowerCase()));
1553
- const matchingPhases = lessonFlow.phases.filter(
1554
- (p) => clusterPhaseNames.has(p.phaseName.toLowerCase())
1555
- );
1725
+ const matchingPhases = lessonFlow.phases.filter((p) => clusterPhaseNames.has(p.phaseName.toLowerCase()));
1556
1726
  const lessonExcerpt = matchingPhases.length > 0 ? matchingPhases.map((p) => `### ${p.phaseName}
1557
1727
  ${p.content}`).join("\n\n") : lessonFlow.rawContent.slice(0, 3500);
1558
1728
  const { systemPrompt, userPrompt } = buildSlideBatchPrompt({
@@ -1566,44 +1736,41 @@ ${p.content}`).join("\n\n") : lessonFlow.rawContent.slice(0, 3500);
1566
1736
  languageDirective,
1567
1737
  headingDirective
1568
1738
  });
1569
- const rawBatch = await runCurriculumAIInference(
1570
- [
1571
- { role: "system", content: systemPrompt },
1572
- { role: "user", content: userPrompt }
1573
- ],
1574
- satelliteContext,
1575
- runnerOptions,
1576
- (chunk, type) => {
1577
- onProgress?.("@illustrator", chunk, { type: type || "content", artifactType: "SLIDE" });
1578
- }
1579
- );
1580
- let batchSlides = [];
1581
1739
  try {
1582
- batchSlides = JSON.parse(extractCleanJson(rawBatch));
1583
- } catch {
1584
- try {
1585
- batchSlides = JSON.parse(jsonrepair.jsonrepair(extractCleanJson(rawBatch)));
1586
- } catch (err) {
1587
- console.warn(`[SlideProductionWorkflow] Batch ${cId} JSON parse failed, synthesizing slides:`, err);
1588
- batchSlides = clusterSlides.map((s) => ({
1589
- id: `slide-${s.slideIndex}`,
1590
- layoutId: s.layoutId,
1591
- title: s.title,
1592
- slots: {
1593
- lead: s.pedagogicalGoal,
1594
- bullets: s.contentFocus,
1595
- code: s.codeSnippetIntent || "// Executable demo code"
1596
- },
1597
- notes: `SCRIPT: Spoken explanation for ${s.title}.
1598
- COLD CALL: What happens when this logic runs?
1599
- SCAFFOLDING: Ensure proper syntax and indentation.`
1600
- }));
1601
- }
1602
- }
1603
- if (Array.isArray(batchSlides)) {
1740
+ const batchSlides = await pRetry__default.default(
1741
+ async () => {
1742
+ const slides = await inferStructured(
1743
+ exports.GeneratedSlideArraySchema,
1744
+ systemPrompt,
1745
+ userPrompt,
1746
+ options,
1747
+ `cluster-${cId}`
1748
+ );
1749
+ if (!slides || slides.length === 0) {
1750
+ throw new Error(`Cluster ${cId} returned empty or schema-invalid output`);
1751
+ }
1752
+ return slides;
1753
+ },
1754
+ {
1755
+ retries: maxRetries - 1,
1756
+ onFailedAttempt: (err) => {
1757
+ onProgress?.("@illustrator", `\u26A0\uFE0F C\u1EE5m ${clusterIdx} l\u1EA7n ${err.attemptNumber}/${maxRetries} l\u1ED7i \u2014 retry ri\xEAng c\u1EE5m n\xE0y...`, {
1758
+ type: "warning"
1759
+ });
1760
+ }
1761
+ }
1762
+ );
1604
1763
  allGeneratedSlides.push(...batchSlides);
1764
+ } catch (clusterErr) {
1765
+ failedClusters.push(cId);
1766
+ console.error(`[SlideProductionWorkflow] Cluster ${cId} permanently failed after ${maxRetries} attempts:`, clusterErr?.message || clusterErr);
1605
1767
  }
1606
1768
  }
1769
+ if (failedClusters.length > 0) {
1770
+ throw new Error(
1771
+ `[SlideProductionWorkflow] Failed to generate ${failedClusters.length}/${clusters.length} cluster(s) (clusterId: ${failedClusters.join(", ")}) after ${maxRetries} attempts each. Aborting to prevent placeholder/degraded slide output. Review provider keys/model availability and retry.`
1772
+ );
1773
+ }
1607
1774
  onProgress?.("@illustrator", `[3/4] Chu\u1EA9n h\xF3a b\u1ED1 c\u1EE5c v\xE0 bi\xEAn d\u1ECBch 1920\xD71080 Stage Deck (${allGeneratedSlides.length} slides)...`);
1608
1775
  const normalizer = presentationKitCore?.normalizeSlideSlots;
1609
1776
  const normalizedSlides = allGeneratedSlides.map((s, idx) => {
@@ -1626,7 +1793,7 @@ SCAFFOLDING: Ensure proper syntax and indentation.`
1626
1793
  compiledHtml = compiled.html;
1627
1794
  }
1628
1795
  } catch (compErr) {
1629
- console.warn(`[SlideProductionWorkflow] Failed compiling HTML deck:`, compErr);
1796
+ console.warn(`[SlideProductionWorkflow] Failed compiling HTML deck:`, compErr?.message || compErr);
1630
1797
  }
1631
1798
  }
1632
1799
  const markdownWrapper = `---
@@ -1645,7 +1812,7 @@ date: "${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}"
1645
1812
  ${JSON.stringify(deckJson, null, 2)}
1646
1813
  \`\`\`
1647
1814
  `;
1648
- onProgress?.("@illustrator", `[4/4] Ho\xE0n t\u1EA5t b\u1ED9 tr\xECnh chi\u1EBFu HTML 20 trang \u0111\u1EA1t chu\u1EA9n Fixed 16:9 Stage!`);
1815
+ onProgress?.("@illustrator", `[4/4] Ho\xE0n t\u1EA5t b\u1ED9 tr\xECnh chi\u1EBFu HTML \u0111\u1EA1t chu\u1EA9n Fixed 16:9 Stage!`);
1649
1816
  return {
1650
1817
  deckJson,
1651
1818
  compiledHtml,
@@ -1654,12 +1821,45 @@ ${JSON.stringify(deckJson, null, 2)}
1654
1821
  slideCount: normalizedSlides.length
1655
1822
  };
1656
1823
  }
1824
+ exports.SlideBlueprintItemSchema = void 0; exports.SlideBlueprintArraySchema = void 0; exports.GeneratedSlideSchema = void 0; exports.GeneratedSlideArraySchema = void 0;
1657
1825
  var init_slideProductionWorkflow = __esm({
1658
1826
  "src/services/slideProductionWorkflow.ts"() {
1659
1827
  init_lessonFlowParser();
1660
1828
  init_slideBlueprintPrompt();
1661
1829
  init_slideBatchPrompt();
1830
+ init_provider_factory();
1662
1831
  init_streamRunner();
1832
+ exports.SlideBlueprintItemSchema = zod.z.object({
1833
+ slideIndex: zod.z.number().int().min(1),
1834
+ clusterId: zod.z.number().int().min(1).default(1),
1835
+ clusterTitle: zod.z.string().default("Cluster"),
1836
+ lessonPhase: zod.z.string().default("Content"),
1837
+ layoutId: zod.z.enum([
1838
+ "hero-cover",
1839
+ "split-concept-code",
1840
+ "two-columns-compare",
1841
+ "three-cards-grid",
1842
+ "timeline-steps",
1843
+ "metric-callout",
1844
+ "checkpoint-quiz",
1845
+ "tiered-practice-3cards",
1846
+ "summary-takeaways"
1847
+ ]),
1848
+ title: zod.z.string().min(1),
1849
+ pedagogicalGoal: zod.z.string().default(""),
1850
+ contentFocus: zod.z.array(zod.z.string()).default([]),
1851
+ codeSnippetIntent: zod.z.string().optional(),
1852
+ visualIntent: zod.z.string().optional()
1853
+ });
1854
+ exports.SlideBlueprintArraySchema = zod.z.array(exports.SlideBlueprintItemSchema);
1855
+ exports.GeneratedSlideSchema = zod.z.object({
1856
+ id: zod.z.string().optional(),
1857
+ layoutId: zod.z.string(),
1858
+ title: zod.z.string().min(1),
1859
+ slots: zod.z.record(zod.z.any()).default({}),
1860
+ notes: zod.z.string().default("")
1861
+ });
1862
+ exports.GeneratedSlideArraySchema = zod.z.array(exports.GeneratedSlideSchema);
1663
1863
  }
1664
1864
  });
1665
1865
  var LearningObjectiveRowSchema = zod.z.object({
@@ -3186,128 +3386,8 @@ var RoadmapInputSchema = zod.z.object({
3186
3386
  phases: zod.z.array(PhaseInputSchema).default([])
3187
3387
  });
3188
3388
 
3189
- // src/ai/provider-factory.ts
3190
- init_streamRunner();
3191
- var DEFAULT_MODELS = {
3192
- nvidia: "nvidia/nemotron-3-ultra-550b-a55b:free",
3193
- openrouter: "@preset/coding-free",
3194
- alibaba: "qwen3.7-plus",
3195
- dashscope: "qwen3.7-plus",
3196
- google: "gemini-1.5-flash",
3197
- deepseek: "deepseek-chat",
3198
- openai: "gpt-4o-mini",
3199
- ollama: "deepseek-v4-flash:0731"
3200
- };
3201
- var NVIDIA_MODELS = {
3202
- codingFast: "nvidia/nemotron-3.5-lightning-30b-a3b",
3203
- reasoningUltra: "nvidia/nemotron-3-ultra-550b-a55b:free",
3204
- superMoE: "nvidia/nemotron-3-super-120b-a12b",
3205
- llamaNemotron: "nvidia/llama-3.1-nemotron-70b-instruct",
3206
- translation: "nvidia/riva-translate-4b-instruct-v2",
3207
- embedding: "nvidia/nemotron-3-embed-1b",
3208
- visionLlama: "meta/llama-3.2-11b-vision-instruct"
3209
- };
3210
- function getAIModel(options = {}) {
3211
- const designated = getDesignatedFallbackConfig();
3212
- const defaultProvider = process.env.DEFAULT_AI_PROVIDER || process.env.LLM_PROVIDER || detectDefaultProvider();
3213
- let provider = (options.provider || defaultProvider).toLowerCase();
3214
- if (!isProviderEnabled(provider)) {
3215
- console.warn(`[provider-factory] Provider "${provider}" is disabled via environment toggle. Falling back to designated provider "${designated.provider}".`);
3216
- provider = designated.provider;
3217
- }
3218
- let resolvedModelName = options.modelName || process.env.DEFAULT_AI_MODEL || process.env.LLM_MODEL || DEFAULT_MODELS[provider] || designated.model;
3219
- if (!isModelAllowed(resolvedModelName)) {
3220
- console.warn(`[provider-factory] Model "${resolvedModelName}" is not permitted by ALLOWED_AI_MODELS. Using designated model "${designated.model}".`);
3221
- resolvedModelName = designated.model;
3222
- }
3223
- if (provider === "nvidia") {
3224
- const apiKey = options.apiKey || process.env.NVIDIA_API_KEY || "";
3225
- const nvidia = openai.createOpenAI({
3226
- apiKey,
3227
- baseURL: options.baseURL || "https://integrate.api.nvidia.com/v1"
3228
- });
3229
- return nvidia.chat(resolvedModelName || "nvidia/nemotron-3-ultra-550b-a55b:free");
3230
- }
3231
- if (provider === "openrouter") {
3232
- const apiKey = options.apiKey || process.env.OPENROUTER_API_KEY || process.env.OPENAI_API_KEY || "";
3233
- const openrouter = openai.createOpenAI({
3234
- apiKey,
3235
- baseURL: options.baseURL || "https://openrouter.ai/api/v1"
3236
- });
3237
- return openrouter.chat(resolvedModelName || "@preset/coding-free");
3238
- }
3239
- if (provider === "alibaba" || provider === "dashscope") {
3240
- const effectiveKey = options.apiKey || process.env.ALIBABA_API_KEY || process.env.DASHSCOPE_API_KEY || process.env.SAAS_ALIBABA_API_KEY || process.env.SAAS_DASHSCOPE_API_KEY || "";
3241
- let defaultBaseURL = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
3242
- if (effectiveKey.startsWith("sk-sp-")) {
3243
- defaultBaseURL = "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1";
3244
- }
3245
- const rawUrl = options.baseURL || process.env.ALIBABA_BASE_URL || process.env.DASHSCOPE_BASE_URL || defaultBaseURL;
3246
- const cleanBaseURL = rawUrl.replace(/^["']|["']$/g, "").trim();
3247
- const alibaba = openai.createOpenAI({
3248
- apiKey: effectiveKey,
3249
- baseURL: cleanBaseURL
3250
- // Passthrough: no forced format injection — the Vercel AI SDK's generateObject
3251
- // already instructs the model to return JSON via its schema definition.
3252
- // Injecting 'Output format: JSON.' into markdown-based lesson prompts caused
3253
- // model confusion (mixed markdown + JSON output).
3254
- });
3255
- return alibaba.chat(resolvedModelName);
3256
- }
3257
- if (provider === "google") {
3258
- const apiKey = options.apiKey || process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY || "";
3259
- const google$1 = google.createGoogleGenerativeAI({ apiKey });
3260
- return google$1(resolvedModelName);
3261
- }
3262
- if (provider === "deepseek") {
3263
- const apiKey = options.apiKey || process.env.DEEPSEEK_API_KEY || process.env.SAAS_DEEPSEEK_API_KEY || "";
3264
- const deepseek$1 = deepseek.createDeepSeek({ apiKey });
3265
- return deepseek$1(resolvedModelName);
3266
- }
3267
- if (provider === "openai") {
3268
- const apiKey = options.apiKey || process.env.OPENAI_API_KEY || "";
3269
- const openai$1 = openai.createOpenAI({
3270
- apiKey,
3271
- baseURL: options.baseURL || process.env.OPENAI_BASE_URL
3272
- });
3273
- return openai$1.chat(resolvedModelName);
3274
- }
3275
- if (provider === "ollama") {
3276
- const apiKey = options.apiKey || process.env.OLLAMA_API_KEY || "ollama";
3277
- const baseURL = options.baseURL || process.env.OLLAMA_BASE_URL || process.env.OPENAI_BASE_URL || "http://127.0.0.1:11434/v1";
3278
- const ollama = openai.createOpenAI({
3279
- apiKey,
3280
- baseURL
3281
- });
3282
- return ollama.chat(resolvedModelName || "qwen2.5:latest");
3283
- }
3284
- const fallback = openai.createOpenAI({
3285
- apiKey: options.apiKey || process.env.OPENAI_API_KEY || "",
3286
- baseURL: options.baseURL || process.env.OPENAI_BASE_URL
3287
- });
3288
- return fallback.chat(resolvedModelName);
3289
- }
3290
- function detectDefaultProvider() {
3291
- if (process.env.NVIDIA_API_KEY && isProviderEnabled("nvidia")) {
3292
- return "nvidia";
3293
- }
3294
- if (process.env.OPENROUTER_API_KEY && isProviderEnabled("openrouter")) {
3295
- return "openrouter";
3296
- }
3297
- if ((process.env.ALIBABA_API_KEY || process.env.DASHSCOPE_API_KEY || process.env.SAAS_ALIBABA_API_KEY) && isProviderEnabled("alibaba")) {
3298
- return "alibaba";
3299
- }
3300
- if ((process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY) && (isProviderEnabled("gemini") || isProviderEnabled("google"))) {
3301
- return "google";
3302
- }
3303
- if ((process.env.DEEPSEEK_API_KEY || process.env.SAAS_DEEPSEEK_API_KEY) && isProviderEnabled("deepseek")) {
3304
- return "deepseek";
3305
- }
3306
- if (process.env.OPENAI_API_KEY && process.env.OPENAI_API_KEY !== "ollama" && isProviderEnabled("openai")) {
3307
- return "openai";
3308
- }
3309
- return getDesignatedFallbackConfig().provider;
3310
- }
3389
+ // src/ai/index.ts
3390
+ init_provider_factory();
3311
3391
 
3312
3392
  // src/templates/lessonTemplate.ts
3313
3393
  var LESSON_PLAN_TEMPLATE = `
@@ -6433,6 +6513,9 @@ Return a single valid JSON object matching DiagnosticQuizSchema with these exact
6433
6513
  }
6434
6514
  `.trim();
6435
6515
  }
6516
+
6517
+ // src/ai/flows/generateLessonMasterFlow.ts
6518
+ init_provider_factory();
6436
6519
  async function generateLessonMasterFlow(input) {
6437
6520
  const model = getAIModel(input.modelOptions);
6438
6521
  const prompt = buildLessonMasterPrompt({
@@ -6452,6 +6535,9 @@ Respond with a valid JSON object matching the schema.`
6452
6535
  });
6453
6536
  return object;
6454
6537
  }
6538
+
6539
+ // src/ai/flows/generateActivityFlow.ts
6540
+ init_provider_factory();
6455
6541
  async function generateActivityFlow(input) {
6456
6542
  const model = getAIModel(input.modelOptions);
6457
6543
  const prompt = buildActivityPrompt({
@@ -6469,6 +6555,9 @@ Respond with a valid JSON object matching the schema.`
6469
6555
  });
6470
6556
  return object;
6471
6557
  }
6558
+
6559
+ // src/ai/flows/generateCodeLabFlow.ts
6560
+ init_provider_factory();
6472
6561
  async function generateCodeLabFlow(input) {
6473
6562
  const model = getAIModel(input.modelOptions);
6474
6563
  const prompt = buildCodeLabPrompt({
@@ -6486,6 +6575,9 @@ Respond with a valid JSON object matching the schema.`
6486
6575
  });
6487
6576
  return object;
6488
6577
  }
6578
+
6579
+ // src/ai/flows/generateWorksheetFlow.ts
6580
+ init_provider_factory();
6489
6581
  async function generateWorksheetFlow(input) {
6490
6582
  const model = getAIModel(input.modelOptions);
6491
6583
  const prompt = buildWorksheetPrompt({
@@ -6499,6 +6591,9 @@ async function generateWorksheetFlow(input) {
6499
6591
  });
6500
6592
  return object;
6501
6593
  }
6594
+
6595
+ // src/ai/flows/generateHandoutFlow.ts
6596
+ init_provider_factory();
6502
6597
  async function generateHandoutFlow(input) {
6503
6598
  const model = getAIModel(input.modelOptions);
6504
6599
  const prompt = buildHandoutPrompt({
@@ -6514,6 +6609,9 @@ Respond with a valid JSON object matching the schema.`
6514
6609
  });
6515
6610
  return object;
6516
6611
  }
6612
+
6613
+ // src/ai/flows/generateSlidesFlow.ts
6614
+ init_provider_factory();
6517
6615
  async function generateSlidesFlow(input) {
6518
6616
  const model = getAIModel(input.modelOptions);
6519
6617
  const prompt = buildSlidesPrompt({
@@ -6528,6 +6626,9 @@ async function generateSlidesFlow(input) {
6528
6626
  });
6529
6627
  return object;
6530
6628
  }
6629
+
6630
+ // src/ai/flows/generateTeacherGuideFlow.ts
6631
+ init_provider_factory();
6531
6632
  async function generateTeacherGuideFlow(input) {
6532
6633
  const model = getAIModel(input.modelOptions);
6533
6634
  const prompt = buildTeacherGuidePrompt({
@@ -6542,6 +6643,9 @@ async function generateTeacherGuideFlow(input) {
6542
6643
  });
6543
6644
  return object;
6544
6645
  }
6646
+
6647
+ // src/ai/flows/generateExtensionFlow.ts
6648
+ init_provider_factory();
6545
6649
  async function generateExtensionFlow(input) {
6546
6650
  const model = getAIModel(input.modelOptions);
6547
6651
  const prompt = buildExtensionPrompt({
@@ -6556,6 +6660,9 @@ async function generateExtensionFlow(input) {
6556
6660
  });
6557
6661
  return object;
6558
6662
  }
6663
+
6664
+ // src/ai/flows/auditCurriculumQualityFlow.ts
6665
+ init_provider_factory();
6559
6666
  async function auditCurriculumQualityFlow(input) {
6560
6667
  const model = getAIModel(input.modelOptions);
6561
6668
  const prompt = buildJudgePrompt({
@@ -6575,6 +6682,9 @@ Respond with a valid JSON object matching the schema.`
6575
6682
  });
6576
6683
  return object;
6577
6684
  }
6685
+
6686
+ // src/ai/flows/generateProjectInstructionFlow.ts
6687
+ init_provider_factory();
6578
6688
  async function generateProjectInstructionFlow(input) {
6579
6689
  const model = getAIModel(input.modelOptions);
6580
6690
  const prompt = buildProjectInstructionPrompt({
@@ -6595,6 +6705,9 @@ Respond with a valid JSON object matching the schema.`
6595
6705
  });
6596
6706
  return object;
6597
6707
  }
6708
+
6709
+ // src/ai/flows/generateSelfLabFlow.ts
6710
+ init_provider_factory();
6598
6711
  async function generateSelfLabFlow(input) {
6599
6712
  const model = getAIModel(input.modelOptions);
6600
6713
  const prompt = buildSelfLabPrompt({
@@ -6613,6 +6726,9 @@ Respond with a valid JSON object matching the schema.`
6613
6726
  });
6614
6727
  return object;
6615
6728
  }
6729
+
6730
+ // src/ai/flows/generateDiagnosticQuizFlow.ts
6731
+ init_provider_factory();
6616
6732
  async function generateDiagnosticQuizFlow(input) {
6617
6733
  const model = getAIModel(input.modelOptions);
6618
6734
  const prompt = buildDiagnosticQuizPrompt({
@@ -6630,6 +6746,9 @@ Respond with a valid JSON object matching the schema.`
6630
6746
  });
6631
6747
  return object;
6632
6748
  }
6749
+
6750
+ // src/ai/flows/streamLessonMasterFlow.ts
6751
+ init_provider_factory();
6633
6752
  async function streamLessonMasterFlow(input) {
6634
6753
  const model = getAIModel(input.modelOptions);
6635
6754
  const prompt = buildLessonMasterPrompt({
@@ -6647,6 +6766,9 @@ async function streamLessonMasterFlow(input) {
6647
6766
  Respond with a valid JSON object matching the schema.`
6648
6767
  });
6649
6768
  }
6769
+
6770
+ // src/ai/flows/streamSelfLabFlow.ts
6771
+ init_provider_factory();
6650
6772
  async function streamSelfLabFlow(input) {
6651
6773
  const model = getAIModel(input.modelOptions);
6652
6774
  const prompt = buildSelfLabPrompt({
@@ -6663,6 +6785,9 @@ async function streamSelfLabFlow(input) {
6663
6785
  Respond with a valid JSON object matching the schema.`
6664
6786
  });
6665
6787
  }
6788
+
6789
+ // src/ai/flows/streamDiagnosticQuizFlow.ts
6790
+ init_provider_factory();
6666
6791
  async function streamDiagnosticQuizFlow(input) {
6667
6792
  const model = getAIModel(input.modelOptions);
6668
6793
  const prompt = buildDiagnosticQuizPrompt({
@@ -11344,6 +11469,17 @@ function packUnitSessions(group, nodeById, spec, warnings) {
11344
11469
  const node = nodeById.get(id);
11345
11470
  const isKnowledge = node.kind === "concept";
11346
11471
  if (node.estimated_minutes > spec.contentBudget) {
11472
+ if (node.estimated_minutes <= spec.sessionDurationMinutes || node.estimated_minutes <= spec.contentBudget * 1.25) {
11473
+ if (current.entries.length > 0) flush();
11474
+ const entry = { id, minutes: spec.contentBudget, part: 1, totalParts: 1 };
11475
+ if (isKnowledge) current.knowledgeMinutes += entry.minutes;
11476
+ else current.practiceMinutes += entry.minutes;
11477
+ current.entries.push(entry);
11478
+ current.nodeIds.push(id);
11479
+ current.oversized = false;
11480
+ flush();
11481
+ continue;
11482
+ }
11347
11483
  const parts = [];
11348
11484
  let remaining = node.estimated_minutes;
11349
11485
  while (remaining > 0) {
@@ -11512,6 +11648,35 @@ async function buildCurriculumPlan(rawGraph, options) {
11512
11648
  const unitIndexByGroup = new Map(groups.map((g, i) => [g.key, i]));
11513
11649
  const packed = [];
11514
11650
  for (const g of groups) packed.push(...packUnitSessions(g, nodeById, spec, warnings));
11651
+ if (constraints.total_sessions && packed.length > constraints.total_sessions) {
11652
+ warnings.push(`Initial packing produced ${packed.length} sessions, coalescing to satisfy total_sessions constraint (${constraints.total_sessions})`);
11653
+ for (let i = 0; i < packed.length - 1 && packed.length > constraints.total_sessions; i++) {
11654
+ const curr = packed[i];
11655
+ const next = packed[i + 1];
11656
+ if (curr.groupId === next.groupId) {
11657
+ const currMins = curr.knowledgeMinutes + curr.practiceMinutes;
11658
+ const nextMins = next.knowledgeMinutes + next.practiceMinutes;
11659
+ const totalContent = currMins + nextMins;
11660
+ if (currMins <= 45 || nextMins <= 45 || totalContent <= spec.contentBudget * 1.5) {
11661
+ const scale = totalContent > spec.contentBudget ? spec.contentBudget / totalContent : 1;
11662
+ curr.nodeIds.push(...next.nodeIds);
11663
+ for (const e of next.entries) {
11664
+ e.minutes = Math.max(5, Math.round(e.minutes * scale));
11665
+ curr.entries.push(e);
11666
+ }
11667
+ for (const e of curr.entries) {
11668
+ e.minutes = Math.max(5, Math.round(e.minutes * scale));
11669
+ }
11670
+ const scaledKnowledge = Math.round((curr.knowledgeMinutes + next.knowledgeMinutes) * scale);
11671
+ curr.knowledgeMinutes = scaledKnowledge;
11672
+ curr.practiceMinutes = Math.max(0, spec.contentBudget - scaledKnowledge);
11673
+ curr.oversized = false;
11674
+ packed.splice(i + 1, 1);
11675
+ i--;
11676
+ }
11677
+ }
11678
+ }
11679
+ }
11515
11680
  packed.map(
11516
11681
  (_, i) => "U" + pad2(Math.min(units.length, 99)).replace("U", "U")
11517
11682
  /* placeholder replaced below */
@@ -27732,6 +27897,7 @@ var DeterministicPipelineRunner = class {
27732
27897
 
27733
27898
  // src/services/prefillService.ts
27734
27899
  init_streamRunner();
27900
+ init_provider_factory();
27735
27901
  var DEFAULT_STREAM_IDLE_MS = 18e4;
27736
27902
  var DEFAULT_STREAM_TOTAL_MS = 48e4;
27737
27903
  var LAYER_IDLE_BUDGET_MS = {
@@ -30951,7 +31117,6 @@ exports.MilestoneInputSchema = MilestoneInputSchema;
30951
31117
  exports.MisconceptionEvaluator = MisconceptionEvaluator;
30952
31118
  exports.MisconceptionSchema = MisconceptionSchema;
30953
31119
  exports.ModelPolicyResolver = ModelPolicyResolver;
30954
- exports.NVIDIA_MODELS = NVIDIA_MODELS;
30955
31120
  exports.NodeKindSchema = NodeKindSchema;
30956
31121
  exports.OrganizerTypeEnum = OrganizerTypeEnum;
30957
31122
  exports.PROJECT_INSTRUCTION_TEMPLATE = PROJECT_INSTRUCTION_TEMPLATE;