ima2-gen 1.1.7 → 1.1.9

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.
Files changed (229) hide show
  1. package/README.md +56 -27
  2. package/bin/commands/annotate.js +137 -0
  3. package/bin/commands/annotate.ts +118 -0
  4. package/bin/commands/cancel.js +37 -33
  5. package/bin/commands/cancel.ts +45 -0
  6. package/bin/commands/canvas-versions.js +91 -0
  7. package/bin/commands/canvas-versions.ts +80 -0
  8. package/bin/commands/cardnews.js +293 -0
  9. package/bin/commands/cardnews.ts +248 -0
  10. package/bin/commands/comfy.js +63 -0
  11. package/bin/commands/comfy.ts +54 -0
  12. package/bin/commands/config.js +270 -0
  13. package/bin/commands/config.ts +265 -0
  14. package/bin/commands/edit.js +97 -72
  15. package/bin/commands/edit.ts +116 -0
  16. package/bin/commands/gen.js +140 -118
  17. package/bin/commands/gen.ts +176 -0
  18. package/bin/commands/history.js +164 -0
  19. package/bin/commands/history.ts +145 -0
  20. package/bin/commands/ls.js +60 -42
  21. package/bin/commands/ls.ts +60 -0
  22. package/bin/commands/metadata.js +45 -0
  23. package/bin/commands/metadata.ts +36 -0
  24. package/bin/commands/multimode.js +159 -0
  25. package/bin/commands/multimode.ts +146 -0
  26. package/bin/commands/node.js +176 -0
  27. package/bin/commands/node.ts +157 -0
  28. package/bin/commands/observability.js +201 -0
  29. package/bin/commands/observability.ts +176 -0
  30. package/bin/commands/ping.js +26 -20
  31. package/bin/commands/ping.ts +29 -0
  32. package/bin/commands/prompt.js +506 -0
  33. package/bin/commands/prompt.ts +421 -0
  34. package/bin/commands/ps.js +78 -71
  35. package/bin/commands/ps.ts +78 -0
  36. package/bin/commands/session.js +308 -0
  37. package/bin/commands/session.ts +265 -0
  38. package/bin/commands/show.js +75 -40
  39. package/bin/commands/show.ts +69 -0
  40. package/bin/ima2.js +324 -310
  41. package/bin/ima2.ts +444 -0
  42. package/bin/lib/args.js +75 -66
  43. package/bin/lib/args.ts +73 -0
  44. package/bin/lib/browser-id.js +15 -0
  45. package/bin/lib/browser-id.ts +16 -0
  46. package/bin/lib/client.js +91 -83
  47. package/bin/lib/client.ts +109 -0
  48. package/bin/lib/error-hints.js +14 -17
  49. package/bin/lib/error-hints.ts +23 -0
  50. package/bin/lib/files.js +26 -28
  51. package/bin/lib/files.ts +39 -0
  52. package/bin/lib/output.js +44 -42
  53. package/bin/lib/output.ts +58 -0
  54. package/bin/lib/platform.js +60 -56
  55. package/bin/lib/platform.ts +97 -0
  56. package/bin/lib/sse.js +73 -0
  57. package/bin/lib/sse.ts +73 -0
  58. package/bin/lib/star-prompt.js +69 -76
  59. package/bin/lib/star-prompt.ts +97 -0
  60. package/bin/lib/storage-doctor.js +34 -35
  61. package/bin/lib/storage-doctor.ts +38 -0
  62. package/config.js +147 -190
  63. package/config.ts +331 -0
  64. package/docs/API.md +48 -8
  65. package/docs/CLI.md +190 -0
  66. package/docs/FAQ.ko.md +5 -5
  67. package/docs/FAQ.md +5 -5
  68. package/docs/README.ja.md +71 -25
  69. package/docs/README.ko.md +61 -24
  70. package/docs/README.zh-CN.md +73 -27
  71. package/lib/assetLifecycle.js +130 -130
  72. package/lib/assetLifecycle.ts +142 -0
  73. package/lib/canvasVersionStore.js +135 -153
  74. package/lib/canvasVersionStore.ts +181 -0
  75. package/lib/cardNewsGenerator.js +127 -142
  76. package/lib/cardNewsGenerator.ts +162 -0
  77. package/lib/cardNewsJobStore.js +78 -84
  78. package/lib/cardNewsJobStore.ts +107 -0
  79. package/lib/cardNewsManifestStore.js +88 -93
  80. package/lib/cardNewsManifestStore.ts +112 -0
  81. package/lib/cardNewsPlanner.js +157 -152
  82. package/lib/cardNewsPlanner.ts +180 -0
  83. package/lib/cardNewsPlannerClient.js +101 -98
  84. package/lib/cardNewsPlannerClient.ts +114 -0
  85. package/lib/cardNewsPlannerPrompt.js +56 -56
  86. package/lib/cardNewsPlannerPrompt.ts +60 -0
  87. package/lib/cardNewsPlannerSchema.js +231 -223
  88. package/lib/cardNewsPlannerSchema.ts +259 -0
  89. package/lib/cardNewsRoleTemplateStore.js +39 -41
  90. package/lib/cardNewsRoleTemplateStore.ts +47 -0
  91. package/lib/cardNewsTemplateStore.js +171 -175
  92. package/lib/cardNewsTemplateStore.ts +210 -0
  93. package/lib/codexDetect.js +44 -47
  94. package/lib/codexDetect.ts +69 -0
  95. package/lib/comfyBridge.js +164 -184
  96. package/lib/comfyBridge.ts +214 -0
  97. package/lib/db.js +41 -51
  98. package/lib/db.ts +166 -0
  99. package/lib/errorClassify.js +62 -78
  100. package/lib/errorClassify.ts +100 -0
  101. package/lib/generationErrors.js +140 -103
  102. package/lib/generationErrors.ts +125 -0
  103. package/lib/historyList.js +149 -147
  104. package/lib/historyList.ts +164 -0
  105. package/lib/imageMetadata.js +86 -89
  106. package/lib/imageMetadata.ts +111 -0
  107. package/lib/imageMetadataStore.js +46 -51
  108. package/lib/imageMetadataStore.ts +67 -0
  109. package/lib/imageModels.js +38 -45
  110. package/lib/imageModels.ts +52 -0
  111. package/lib/inflight.js +131 -150
  112. package/lib/inflight.ts +204 -0
  113. package/lib/localImportStore.js +105 -0
  114. package/lib/localImportStore.ts +111 -0
  115. package/lib/logger.js +105 -112
  116. package/lib/logger.ts +150 -0
  117. package/lib/nodeStore.js +65 -64
  118. package/lib/nodeStore.ts +81 -0
  119. package/lib/oauthLauncher.js +61 -59
  120. package/lib/oauthLauncher.ts +64 -0
  121. package/lib/oauthNormalize.js +15 -19
  122. package/lib/oauthNormalize.ts +30 -0
  123. package/lib/oauthProxy.js +834 -832
  124. package/lib/oauthProxy.ts +995 -0
  125. package/lib/openDirectory.js +41 -40
  126. package/lib/openDirectory.ts +45 -0
  127. package/lib/pngInfo.js +18 -20
  128. package/lib/pngInfo.ts +26 -0
  129. package/lib/promptImport/curatedSources.js +135 -0
  130. package/lib/promptImport/curatedSources.ts +139 -0
  131. package/lib/promptImport/discoveryRegistry.js +218 -0
  132. package/lib/promptImport/discoveryRegistry.ts +236 -0
  133. package/lib/promptImport/errors.js +10 -10
  134. package/lib/promptImport/errors.ts +18 -0
  135. package/lib/promptImport/githubDiscovery.js +238 -0
  136. package/lib/promptImport/githubDiscovery.ts +248 -0
  137. package/lib/promptImport/githubFolder.js +302 -0
  138. package/lib/promptImport/githubFolder.ts +308 -0
  139. package/lib/promptImport/githubSource.js +194 -171
  140. package/lib/promptImport/githubSource.ts +239 -0
  141. package/lib/promptImport/gptImageHints.js +61 -0
  142. package/lib/promptImport/gptImageHints.ts +68 -0
  143. package/lib/promptImport/parsePromptCandidates.js +110 -112
  144. package/lib/promptImport/parsePromptCandidates.ts +153 -0
  145. package/lib/promptImport/promptIndex.js +230 -0
  146. package/lib/promptImport/promptIndex.ts +248 -0
  147. package/lib/promptImport/rankPromptCandidates.js +52 -0
  148. package/lib/promptImport/rankPromptCandidates.ts +49 -0
  149. package/lib/providerOptions.js +31 -0
  150. package/lib/providerOptions.ts +41 -0
  151. package/lib/referenceImageCompress.js +51 -62
  152. package/lib/referenceImageCompress.ts +75 -0
  153. package/lib/refs.js +93 -81
  154. package/lib/refs.ts +117 -0
  155. package/lib/requestLogger.js +32 -38
  156. package/lib/requestLogger.ts +48 -0
  157. package/lib/responsesImageAdapter.js +351 -0
  158. package/lib/responsesImageAdapter.ts +352 -0
  159. package/lib/runtimePorts.js +71 -73
  160. package/lib/runtimePorts.ts +93 -0
  161. package/lib/sessionStore.js +179 -230
  162. package/lib/sessionStore.ts +272 -0
  163. package/lib/storageMigration.js +247 -245
  164. package/lib/storageMigration.ts +284 -0
  165. package/lib/styleSheet.js +86 -90
  166. package/lib/styleSheet.ts +128 -0
  167. package/lib/systemTrash.js +18 -0
  168. package/lib/systemTrash.ts +20 -0
  169. package/package.json +26 -10
  170. package/routes/annotations.js +76 -79
  171. package/routes/annotations.ts +95 -0
  172. package/routes/canvasVersions.js +50 -54
  173. package/routes/canvasVersions.ts +64 -0
  174. package/routes/cardNews.js +158 -171
  175. package/routes/cardNews.ts +183 -0
  176. package/routes/comfy.js +23 -31
  177. package/routes/comfy.ts +39 -0
  178. package/routes/edit.js +183 -214
  179. package/routes/edit.ts +230 -0
  180. package/routes/generate.js +269 -291
  181. package/routes/generate.ts +309 -0
  182. package/routes/health.js +102 -107
  183. package/routes/health.ts +114 -0
  184. package/routes/history.js +136 -144
  185. package/routes/history.ts +153 -0
  186. package/routes/imageImport.js +33 -0
  187. package/routes/imageImport.ts +33 -0
  188. package/routes/index.js +18 -16
  189. package/routes/index.ts +35 -0
  190. package/routes/metadata.js +60 -64
  191. package/routes/metadata.ts +71 -0
  192. package/routes/multimode.js +228 -263
  193. package/routes/multimode.ts +280 -0
  194. package/routes/nodes.js +378 -424
  195. package/routes/nodes.ts +455 -0
  196. package/routes/promptImport.js +291 -152
  197. package/routes/promptImport.ts +354 -0
  198. package/routes/prompts.js +333 -360
  199. package/routes/prompts.ts +379 -0
  200. package/routes/sessions.js +277 -285
  201. package/routes/sessions.ts +292 -0
  202. package/routes/storage.js +29 -31
  203. package/routes/storage.ts +39 -0
  204. package/server.js +189 -196
  205. package/server.ts +235 -0
  206. package/ui/dist/.vite/manifest.json +101 -0
  207. package/ui/dist/assets/CardNewsWorkspace-BJOCey7Z.js +2 -0
  208. package/ui/dist/assets/NodeCanvas-BZV40eAE.css +1 -0
  209. package/ui/dist/assets/NodeCanvas-C3dzYNsk.js +7 -0
  210. package/ui/dist/assets/PromptImportDialog-Dqu1VpUh.js +2 -0
  211. package/ui/dist/assets/PromptImportDiscoverySection-Dg8T9X0L.js +1 -0
  212. package/ui/dist/assets/PromptImportFolderSection-DBaqsFO4.js +1 -0
  213. package/ui/dist/assets/PromptLibraryPanel-p5QqR97M.js +2 -0
  214. package/ui/dist/assets/SettingsWorkspace-B5bSAZ6u.js +1 -0
  215. package/ui/dist/assets/index-C9cXwiWE.js +25 -0
  216. package/ui/dist/assets/index-CGMIkZXn.css +1 -0
  217. package/ui/dist/assets/index-Cvld7dUZ.js +1 -0
  218. package/ui/dist/index.html +6 -3
  219. package/assets/screenshot.png +0 -0
  220. package/assets/screenshots/classic-generate-light.png +0 -0
  221. package/assets/screenshots/node-graph-branching.png +0 -0
  222. package/assets/screenshots/settings-oauth-generation.png +0 -0
  223. package/assets/screenshots/settings-workspace.png +0 -0
  224. package/assets/screenshots/style-sheet-editor.png +0 -0
  225. package/integrations/comfyui/ima2_gen_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
  226. package/integrations/comfyui/ima2_gen_bridge/__pycache__/nodes.cpython-313.pyc +0 -0
  227. package/ui/dist/assets/index-DARPdT4Q.css +0 -1
  228. package/ui/dist/assets/index-ht80GMq4.js +0 -31
  229. package/ui/dist/assets/index-ht80GMq4.js.map +0 -1
@@ -5,176 +5,181 @@ import { buildCardNewsPlannerMessages } from "./cardNewsPlannerPrompt.js";
5
5
  import { requestCardNewsPlannerJson } from "./cardNewsPlannerClient.js";
6
6
  import { repairPlannerOutput, validatePlannerOutput } from "./cardNewsPlannerSchema.js";
7
7
  import { waitForOAuthReady } from "./oauthProxy.js";
8
-
9
8
  function compactText(value, fallback) {
10
- const text = typeof value === "string" ? value.trim() : "";
11
- return text || fallback;
9
+ const text = typeof value === "string" ? value.trim() : "";
10
+ return text || fallback;
12
11
  }
13
-
14
12
  function detectBriefLanguage(input) {
15
- const text = [input.topic, input.audience, input.goal, input.contentBrief].filter(Boolean).join(" ");
16
- if (/[가-힣]/.test(text)) return "ko";
17
- if (/[A-Za-z]/.test(text)) return "en";
18
- return "und";
13
+ const text = [input.topic, input.audience, input.goal, input.contentBrief].filter(Boolean).join(" ");
14
+ if (/[가-힣]/.test(text))
15
+ return "ko";
16
+ if (/[A-Za-z]/.test(text))
17
+ return "en";
18
+ return "und";
19
19
  }
20
-
21
20
  function fallbackLabel(role, lang) {
22
- const ko = {
23
- cta: "다음 행동",
24
- problem: "왜 중요한가",
25
- insight: "핵심 인사이트",
26
- example: "예시로 보기",
27
- data: "숫자로 확인",
28
- summary: "요약",
29
- };
30
- const en = {
31
- cta: "Next action",
32
- problem: "Why it matters",
33
- insight: "Key insight",
34
- example: "Example",
35
- data: "By the numbers",
36
- summary: "Summary",
37
- };
38
- if (lang === "ko") return ko[role] || role;
39
- if (lang === "en") return en[role] || role;
40
- return "";
21
+ const ko = {
22
+ cta: "다음 행동",
23
+ problem: "왜 중요한가",
24
+ insight: "핵심 인사이트",
25
+ example: "예시로 보기",
26
+ data: "숫자로 확인",
27
+ summary: "요약",
28
+ };
29
+ const en = {
30
+ cta: "Next action",
31
+ problem: "Why it matters",
32
+ insight: "Key insight",
33
+ example: "Example",
34
+ data: "By the numbers",
35
+ summary: "Summary",
36
+ };
37
+ if (lang === "ko")
38
+ return ko[role] || role;
39
+ if (lang === "en")
40
+ return en[role] || role;
41
+ return "";
41
42
  }
42
-
43
43
  function headlineFor(role, topic, lang) {
44
- const label = compactText(topic, "Card news");
45
- if (role === "cover" || role === "hook") return label;
46
- return fallbackLabel(role, lang) || label;
44
+ const label = compactText(topic, "Card news");
45
+ if (role === "cover" || role === "hook")
46
+ return label;
47
+ return fallbackLabel(role, lang) || label;
47
48
  }
48
-
49
49
  function bodyFor(role, brief, lang) {
50
- const content = compactText(brief.content, "");
51
- if (content) return content;
52
- const target = compactText(brief.audience, lang === "ko" ? "독자" : "reader");
53
- const goal = compactText(brief.goal, lang === "ko" ? "핵심 메시지" : "the key message");
54
- if (lang === "ko") {
55
- if (role === "cta") return `${target}가 바로 실행할 수 있는 다음 단계를 제안합니다.`;
56
- if (role === "problem") return `${target}가 겪는 문제를 짧고 분명하게 보여줍니다.`;
57
- if (role === "insight") return `${goal} 이해하기 쉬운 문장으로 정리합니다.`;
58
- return `${goal}을 카드 역할에 맞춰 전달합니다.`;
59
- }
60
- if (role === "cta") return `Suggest a next step ${target} can take immediately.`;
61
- if (role === "problem") return `Show the problem ${target} faces in a concise way.`;
62
- if (role === "insight") return `Explain ${goal} in one clear sentence.`;
63
- return `Present ${goal} for this card.`;
50
+ const content = compactText(brief.content, "");
51
+ if (content)
52
+ return content;
53
+ const target = compactText(brief.audience, lang === "ko" ? "독자" : "reader");
54
+ const goal = compactText(brief.goal, lang === "ko" ? "핵심 메시지" : "the key message");
55
+ if (lang === "ko") {
56
+ if (role === "cta")
57
+ return `${target} 바로 실행할 있는 다음 단계를 제안합니다.`;
58
+ if (role === "problem")
59
+ return `${target}가 겪는 문제를 짧고 분명하게 보여줍니다.`;
60
+ if (role === "insight")
61
+ return `${goal} 이해하기 쉬운 문장으로 정리합니다.`;
62
+ return `${goal} 카드 역할에 맞춰 전달합니다.`;
63
+ }
64
+ if (role === "cta")
65
+ return `Suggest a next step ${target} can take immediately.`;
66
+ if (role === "problem")
67
+ return `Show the problem ${target} faces in a concise way.`;
68
+ if (role === "insight")
69
+ return `Explain ${goal} in one clear sentence.`;
70
+ return `Present ${goal} for this card.`;
64
71
  }
65
-
66
72
  function normalizeTextFields(fields) {
67
- return Array.isArray(fields) ? fields : [];
73
+ return Array.isArray(fields) ? fields : [];
68
74
  }
69
-
70
75
  function toCardNewsPlan(plannerOutput, input, roleTemplate) {
71
- const topic = compactText(plannerOutput.topic, compactText(input.topic, input.title || "Untitled card news"));
72
- return {
73
- setId: input.setId || `cs_${ulid()}`,
74
- title: compactText(plannerOutput.title, topic),
75
- topic,
76
- imageTemplateId: input.imageTemplateId || "academy-lesson-square",
77
- roleTemplateId: roleTemplate.id,
78
- size: input.size || "2048x2048",
79
- generationStrategy: "parallel-template-i2i",
80
- cards: plannerOutput.cards.map((card, index) => ({
81
- id: `card_${index + 1}`,
82
- order: index + 1,
83
- role: card.role,
84
- headline: card.headline,
85
- body: card.body,
86
- visualPrompt: card.visualPrompt,
87
- textFields: normalizeTextFields(card.textFields),
88
- templateSlotAssignments: {
89
- title: "headline",
90
- body: "body",
91
- image: "visual",
92
- },
93
- references: card.references || [],
94
- locked: false,
95
- status: "draft",
96
- })),
97
- };
76
+ const topic = compactText(plannerOutput.topic, compactText(input.topic, input.title || "Untitled card news"));
77
+ return {
78
+ setId: input.setId || `cs_${ulid()}`,
79
+ title: compactText(plannerOutput.title, topic),
80
+ topic,
81
+ imageTemplateId: input.imageTemplateId || "academy-lesson-square",
82
+ roleTemplateId: roleTemplate.id,
83
+ size: input.size || "2048x2048",
84
+ generationStrategy: "parallel-template-i2i",
85
+ cards: plannerOutput.cards.map((card, index) => ({
86
+ id: `card_${index + 1}`,
87
+ order: index + 1,
88
+ role: card.role,
89
+ headline: card.headline,
90
+ body: card.body,
91
+ visualPrompt: card.visualPrompt,
92
+ textFields: normalizeTextFields(card.textFields),
93
+ templateSlotAssignments: {
94
+ title: "headline",
95
+ body: "body",
96
+ image: "visual",
97
+ },
98
+ references: card.references || [],
99
+ locked: false,
100
+ status: "draft",
101
+ })),
102
+ };
98
103
  }
99
-
100
104
  export function createDeterministicCardNewsDraft(input = {}) {
101
- const roleTemplate = getRoleTemplate(input.roleTemplateId);
102
- const topic = compactText(input.topic, input.title || "Untitled card news");
103
- const title = compactText(input.title, topic);
104
- const brief = {
105
- audience: input.audience,
106
- goal: input.goal,
107
- content: input.contentBrief,
108
- };
109
- const lang = detectBriefLanguage(input);
110
- const output = {
111
- title,
112
- topic,
113
- audience: compactText(input.audience, ""),
114
- goal: compactText(input.goal, ""),
115
- cards: roleTemplate.roles.map((role, idx) => ({
116
- order: idx + 1,
117
- role: role.role,
118
- headline: headlineFor(role.role, topic, lang),
119
- body: bodyFor(role.role, brief, lang),
120
- visualPrompt: `${role.promptHint}, ${topic}`,
121
- textFields: [],
122
- references: [],
123
- locked: false,
124
- })),
125
- };
126
- return toCardNewsPlan(output, input, roleTemplate);
105
+ const roleTemplate = getRoleTemplate(input.roleTemplateId);
106
+ const topic = compactText(input.topic, input.title || "Untitled card news");
107
+ const title = compactText(input.title, topic);
108
+ const brief = {
109
+ audience: input.audience,
110
+ goal: input.goal,
111
+ content: input.contentBrief,
112
+ };
113
+ const lang = detectBriefLanguage(input);
114
+ const output = {
115
+ title,
116
+ topic,
117
+ audience: compactText(input.audience, ""),
118
+ goal: compactText(input.goal, ""),
119
+ cards: roleTemplate.roles.map((role, idx) => ({
120
+ order: idx + 1,
121
+ role: role.role,
122
+ headline: headlineFor(role.role, topic, lang),
123
+ body: bodyFor(role.role, brief, lang),
124
+ visualPrompt: `${role.promptHint}, ${topic}`,
125
+ textFields: [],
126
+ references: [],
127
+ locked: false,
128
+ })),
129
+ };
130
+ return toCardNewsPlan(output, input, roleTemplate);
127
131
  }
128
-
129
132
  function plannerError(message, code, status) {
130
- const err = new Error(message);
131
- err.code = code;
132
- err.status = status;
133
- return err;
133
+ const err = new Error(message);
134
+ err.code = code;
135
+ err.status = status;
136
+ return err;
134
137
  }
135
-
136
138
  export async function createCardNewsDraft(ctxOrInput = {}, maybeInput = {}) {
137
- const hasCtx = !!ctxOrInput?.config;
138
- const ctx = hasCtx ? ctxOrInput : null;
139
- const input = hasCtx ? maybeInput : ctxOrInput;
140
- const roleTemplate = getRoleTemplate(input.roleTemplateId);
141
-
142
- if (!ctx) return createDeterministicCardNewsDraft(input);
143
- if (!ctx.config.cardNewsPlanner?.enabled) {
144
- return {
145
- plan: createDeterministicCardNewsDraft(input),
146
- planner: { mode: "deterministic-fallback", model: "none", repaired: false },
147
- };
148
- }
149
-
150
- const imageTemplate = await getImageTemplate(ctx, input.imageTemplateId || "academy-lesson-square");
151
- try {
152
- await waitForOAuthReady(ctx);
153
- const messages = buildCardNewsPlannerMessages({ ...input, roleTemplate, imageTemplate });
154
- const raw = await requestCardNewsPlannerJson({ messages }, {
155
- oauthUrl: ctx.oauthUrl,
156
- model: ctx.config.cardNewsPlanner.model,
157
- timeoutMs: ctx.config.cardNewsPlanner.timeoutMs,
158
- });
159
- let result = validatePlannerOutput(raw.output, roleTemplate);
160
- if (!result.ok) result = repairPlannerOutput(raw.output, { ...input, roleTemplate });
161
- if (!result.ok) throw plannerError("Planner schema invalid", "PLANNER_SCHEMA_INVALID", 422);
162
- return {
163
- plan: toCardNewsPlan(result.plan, input, roleTemplate),
164
- planner: { mode: raw.mode, model: raw.model, repaired: result.repaired },
165
- };
166
- } catch (err) {
167
- if (ctx.config.cardNewsPlanner.deterministicFallback) {
168
- return {
169
- plan: createDeterministicCardNewsDraft(input),
170
- planner: {
171
- mode: "deterministic-fallback",
172
- model: ctx.config.cardNewsPlanner.model,
173
- repaired: true,
174
- },
175
- };
139
+ const hasCtx = !!ctxOrInput?.config;
140
+ const ctx = hasCtx ? ctxOrInput : null;
141
+ const input = hasCtx ? maybeInput : ctxOrInput;
142
+ const roleTemplate = getRoleTemplate(input.roleTemplateId);
143
+ if (!ctx)
144
+ return createDeterministicCardNewsDraft(input);
145
+ if (!ctx.config.cardNewsPlanner?.enabled) {
146
+ return {
147
+ plan: createDeterministicCardNewsDraft(input),
148
+ planner: { mode: "deterministic-fallback", model: "none", repaired: false },
149
+ };
150
+ }
151
+ const imageTemplate = await getImageTemplate(ctx, input.imageTemplateId || "academy-lesson-square");
152
+ try {
153
+ await waitForOAuthReady(ctx);
154
+ const messages = buildCardNewsPlannerMessages({ ...input, roleTemplate, imageTemplate });
155
+ const raw = await requestCardNewsPlannerJson({ messages }, {
156
+ oauthUrl: ctx.oauthUrl,
157
+ model: ctx.config.cardNewsPlanner.model,
158
+ timeoutMs: ctx.config.cardNewsPlanner.timeoutMs,
159
+ });
160
+ let result = validatePlannerOutput(raw.output, roleTemplate);
161
+ if (!result.ok)
162
+ result = repairPlannerOutput(raw.output, { ...input, roleTemplate });
163
+ if (!result.ok)
164
+ throw plannerError("Planner schema invalid", "PLANNER_SCHEMA_INVALID", 422);
165
+ return {
166
+ plan: toCardNewsPlan(result.plan, input, roleTemplate),
167
+ planner: { mode: raw.mode, model: raw.model, repaired: result.repaired },
168
+ };
169
+ }
170
+ catch (err) {
171
+ if (ctx.config.cardNewsPlanner.deterministicFallback) {
172
+ return {
173
+ plan: createDeterministicCardNewsDraft(input),
174
+ planner: {
175
+ mode: "deterministic-fallback",
176
+ model: ctx.config.cardNewsPlanner.model,
177
+ repaired: true,
178
+ },
179
+ };
180
+ }
181
+ if (err.code)
182
+ throw err;
183
+ throw plannerError(err.message || "Planner unavailable", "PLANNER_UNAVAILABLE", 503);
176
184
  }
177
- if (err.code) throw err;
178
- throw plannerError(err.message || "Planner unavailable", "PLANNER_UNAVAILABLE", 503);
179
- }
180
185
  }
@@ -0,0 +1,180 @@
1
+ import { ulid } from "ulid";
2
+ import { getRoleTemplate } from "./cardNewsRoleTemplateStore.js";
3
+ import { getImageTemplate } from "./cardNewsTemplateStore.js";
4
+ import { buildCardNewsPlannerMessages } from "./cardNewsPlannerPrompt.js";
5
+ import { requestCardNewsPlannerJson } from "./cardNewsPlannerClient.js";
6
+ import { repairPlannerOutput, validatePlannerOutput } from "./cardNewsPlannerSchema.js";
7
+ import { waitForOAuthReady } from "./oauthProxy.js";
8
+
9
+ function compactText(value, fallback) {
10
+ const text = typeof value === "string" ? value.trim() : "";
11
+ return text || fallback;
12
+ }
13
+
14
+ function detectBriefLanguage(input) {
15
+ const text = [input.topic, input.audience, input.goal, input.contentBrief].filter(Boolean).join(" ");
16
+ if (/[가-힣]/.test(text)) return "ko";
17
+ if (/[A-Za-z]/.test(text)) return "en";
18
+ return "und";
19
+ }
20
+
21
+ function fallbackLabel(role, lang) {
22
+ const ko = {
23
+ cta: "다음 행동",
24
+ problem: "왜 중요한가",
25
+ insight: "핵심 인사이트",
26
+ example: "예시로 보기",
27
+ data: "숫자로 확인",
28
+ summary: "요약",
29
+ };
30
+ const en = {
31
+ cta: "Next action",
32
+ problem: "Why it matters",
33
+ insight: "Key insight",
34
+ example: "Example",
35
+ data: "By the numbers",
36
+ summary: "Summary",
37
+ };
38
+ if (lang === "ko") return ko[role] || role;
39
+ if (lang === "en") return en[role] || role;
40
+ return "";
41
+ }
42
+
43
+ function headlineFor(role, topic, lang) {
44
+ const label = compactText(topic, "Card news");
45
+ if (role === "cover" || role === "hook") return label;
46
+ return fallbackLabel(role, lang) || label;
47
+ }
48
+
49
+ function bodyFor(role, brief, lang) {
50
+ const content = compactText(brief.content, "");
51
+ if (content) return content;
52
+ const target = compactText(brief.audience, lang === "ko" ? "독자" : "reader");
53
+ const goal = compactText(brief.goal, lang === "ko" ? "핵심 메시지" : "the key message");
54
+ if (lang === "ko") {
55
+ if (role === "cta") return `${target}가 바로 실행할 수 있는 다음 단계를 제안합니다.`;
56
+ if (role === "problem") return `${target}가 겪는 문제를 짧고 분명하게 보여줍니다.`;
57
+ if (role === "insight") return `${goal}을 이해하기 쉬운 한 문장으로 정리합니다.`;
58
+ return `${goal}을 카드 역할에 맞춰 전달합니다.`;
59
+ }
60
+ if (role === "cta") return `Suggest a next step ${target} can take immediately.`;
61
+ if (role === "problem") return `Show the problem ${target} faces in a concise way.`;
62
+ if (role === "insight") return `Explain ${goal} in one clear sentence.`;
63
+ return `Present ${goal} for this card.`;
64
+ }
65
+
66
+ function normalizeTextFields(fields) {
67
+ return Array.isArray(fields) ? fields : [];
68
+ }
69
+
70
+ function toCardNewsPlan(plannerOutput, input, roleTemplate) {
71
+ const topic = compactText(plannerOutput.topic, compactText(input.topic, input.title || "Untitled card news"));
72
+ return {
73
+ setId: input.setId || `cs_${ulid()}`,
74
+ title: compactText(plannerOutput.title, topic),
75
+ topic,
76
+ imageTemplateId: input.imageTemplateId || "academy-lesson-square",
77
+ roleTemplateId: roleTemplate.id,
78
+ size: input.size || "2048x2048",
79
+ generationStrategy: "parallel-template-i2i",
80
+ cards: plannerOutput.cards.map((card, index) => ({
81
+ id: `card_${index + 1}`,
82
+ order: index + 1,
83
+ role: card.role,
84
+ headline: card.headline,
85
+ body: card.body,
86
+ visualPrompt: card.visualPrompt,
87
+ textFields: normalizeTextFields(card.textFields),
88
+ templateSlotAssignments: {
89
+ title: "headline",
90
+ body: "body",
91
+ image: "visual",
92
+ },
93
+ references: card.references || [],
94
+ locked: false,
95
+ status: "draft",
96
+ })),
97
+ };
98
+ }
99
+
100
+ export function createDeterministicCardNewsDraft(input: any = {}) {
101
+ const roleTemplate = getRoleTemplate(input.roleTemplateId);
102
+ const topic = compactText(input.topic, input.title || "Untitled card news");
103
+ const title = compactText(input.title, topic);
104
+ const brief = {
105
+ audience: input.audience,
106
+ goal: input.goal,
107
+ content: input.contentBrief,
108
+ };
109
+ const lang = detectBriefLanguage(input);
110
+ const output = {
111
+ title,
112
+ topic,
113
+ audience: compactText(input.audience, ""),
114
+ goal: compactText(input.goal, ""),
115
+ cards: roleTemplate.roles.map((role, idx) => ({
116
+ order: idx + 1,
117
+ role: role.role,
118
+ headline: headlineFor(role.role, topic, lang),
119
+ body: bodyFor(role.role, brief, lang),
120
+ visualPrompt: `${role.promptHint}, ${topic}`,
121
+ textFields: [],
122
+ references: [],
123
+ locked: false,
124
+ })),
125
+ };
126
+ return toCardNewsPlan(output, input, roleTemplate);
127
+ }
128
+
129
+ function plannerError(message, code, status) {
130
+ const err: any = new Error(message);
131
+ err.code = code;
132
+ err.status = status;
133
+ return err;
134
+ }
135
+
136
+ export async function createCardNewsDraft(ctxOrInput: any = {}, maybeInput: any = {}) {
137
+ const hasCtx = !!ctxOrInput?.config;
138
+ const ctx = hasCtx ? ctxOrInput : null;
139
+ const input = hasCtx ? maybeInput : ctxOrInput;
140
+ const roleTemplate = getRoleTemplate(input.roleTemplateId);
141
+
142
+ if (!ctx) return createDeterministicCardNewsDraft(input);
143
+ if (!ctx.config.cardNewsPlanner?.enabled) {
144
+ return {
145
+ plan: createDeterministicCardNewsDraft(input),
146
+ planner: { mode: "deterministic-fallback", model: "none", repaired: false },
147
+ };
148
+ }
149
+
150
+ const imageTemplate = await getImageTemplate(ctx, input.imageTemplateId || "academy-lesson-square");
151
+ try {
152
+ await waitForOAuthReady(ctx);
153
+ const messages = buildCardNewsPlannerMessages({ ...input, roleTemplate, imageTemplate });
154
+ const raw = await requestCardNewsPlannerJson({ messages }, {
155
+ oauthUrl: ctx.oauthUrl,
156
+ model: ctx.config.cardNewsPlanner.model,
157
+ timeoutMs: ctx.config.cardNewsPlanner.timeoutMs,
158
+ });
159
+ let result = validatePlannerOutput(raw.output, roleTemplate);
160
+ if (!result.ok) result = repairPlannerOutput(raw.output, { ...input, roleTemplate });
161
+ if (!result.ok) throw plannerError("Planner schema invalid", "PLANNER_SCHEMA_INVALID", 422);
162
+ return {
163
+ plan: toCardNewsPlan(result.plan, input, roleTemplate),
164
+ planner: { mode: raw.mode, model: raw.model, repaired: result.repaired },
165
+ };
166
+ } catch (err) {
167
+ if (ctx.config.cardNewsPlanner.deterministicFallback) {
168
+ return {
169
+ plan: createDeterministicCardNewsDraft(input),
170
+ planner: {
171
+ mode: "deterministic-fallback",
172
+ model: ctx.config.cardNewsPlanner.model,
173
+ repaired: true,
174
+ },
175
+ };
176
+ }
177
+ if (err.code) throw err;
178
+ throw plannerError(err.message || "Planner unavailable", "PLANNER_UNAVAILABLE", 503);
179
+ }
180
+ }