@zzclub/pipeline 0.2.0 → 0.4.0

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 (194) hide show
  1. package/README.md +366 -170
  2. package/package.json +4 -7
  3. package/src/adapter-loader.ts +138 -0
  4. package/src/adapter-types.ts +172 -0
  5. package/src/adapters/builtin-image-renderer.ts +353 -0
  6. package/src/adapters/builtin-markdown-renderer.ts +63 -0
  7. package/src/adapters/index.ts +2 -0
  8. package/src/args.test.ts +118 -0
  9. package/src/args.ts +90 -0
  10. package/src/cli.ts +51 -0
  11. package/src/commands/abandon.test.ts +104 -0
  12. package/src/commands/abandon.ts +202 -0
  13. package/src/commands/attach-body-images.ts +88 -0
  14. package/src/commands/attach-body.ts +86 -0
  15. package/src/commands/attach-newspic-spec.ts +41 -0
  16. package/src/commands/checkpoint.test.ts +91 -0
  17. package/src/commands/checkpoint.ts +66 -0
  18. package/src/commands/config.ts +106 -0
  19. package/src/commands/cos-upload.ts +62 -0
  20. package/src/commands/doctor.ts +66 -0
  21. package/src/commands/find-run.ts +55 -0
  22. package/src/commands/hermes-metrics.ts +226 -0
  23. package/src/commands/imgx.ts +60 -0
  24. package/src/commands/ingest-handoff.ts +509 -0
  25. package/src/commands/init.ts +126 -0
  26. package/src/commands/prepare-finalize.ts +348 -0
  27. package/src/commands/prepare.ts +310 -0
  28. package/src/commands/publish.ts +147 -0
  29. package/src/commands/reconcile.ts +36 -0
  30. package/src/commands/render.ts +244 -0
  31. package/src/commands/reset.test.ts +151 -0
  32. package/src/commands/reset.ts +159 -0
  33. package/src/commands/review.ts +77 -0
  34. package/src/commands/status.ts +68 -0
  35. package/src/commands/sync-blog.ts +39 -0
  36. package/src/commands/tasks.ts +50 -0
  37. package/src/commands/wechat-export.ts +68 -0
  38. package/src/config.ts +0 -0
  39. package/src/imgx/ascii-portrait.ts +77 -0
  40. package/src/imgx/assets/fonts/AlimamaShuHeiTi-Bold.ttf +0 -0
  41. package/src/imgx/assets/fonts/LXGWNeoZhiSongPlus.ttf +0 -0
  42. package/src/imgx/assets/fonts/LXGWWenKai-Regular.ttf +0 -0
  43. package/src/imgx/assets/icons/logo.png +0 -0
  44. package/src/imgx/assets/icons/logo.svg +8 -0
  45. package/{dist/assets → src}/imgx/assets/templates/longform-3-4.html +1 -1
  46. package/{dist/assets → src}/imgx/assets/templates/poster-3-4.html +69 -30
  47. package/{dist/assets → src}/imgx/assets/templates/wechat-cover-split.html +58 -27
  48. package/src/imgx/cli.ts +57 -0
  49. package/src/imgx/geometry.ts +68 -0
  50. package/src/imgx/index.ts +5 -0
  51. package/src/imgx/longform-theme.ts +243 -0
  52. package/src/imgx/poster-recipe.ts +81 -0
  53. package/src/imgx/pretext-adapter.ts +14 -0
  54. package/src/imgx/pretext-runtime.ts +61 -0
  55. package/src/imgx/references/ascii-portrait-3-4.md +59 -0
  56. package/src/imgx/references/longform-3-4.md +173 -0
  57. package/src/imgx/references/poster-3-4.md +87 -0
  58. package/src/imgx/references/tips-3-4.md +69 -0
  59. package/src/imgx/references/wechat-cover-split.md +89 -0
  60. package/src/imgx/references/x-like-posts.md +139 -0
  61. package/src/imgx/render-article.test.ts +53 -0
  62. package/src/imgx/render-article.ts +1364 -0
  63. package/src/imgx/render-ascii-portrait.ts +53 -0
  64. package/src/imgx/render-card.ts +232 -0
  65. package/src/imgx/render-x-like-posts.ts +235 -0
  66. package/src/imgx/runtime.ts +403 -0
  67. package/src/imgx/typographic-scale.test.ts +56 -0
  68. package/src/imgx/typographic-scale.ts +44 -0
  69. package/src/output.ts +632 -0
  70. package/src/plugins.ts +101 -0
  71. package/src/profiles.ts +149 -0
  72. package/src/providers/blog.test.ts +58 -0
  73. package/src/providers/blog.ts +154 -0
  74. package/src/providers/cos.ts +183 -0
  75. package/src/providers/index.test.ts +26 -0
  76. package/src/providers/index.ts +185 -0
  77. package/src/providers/wechat.test.ts +215 -0
  78. package/src/providers/wechat.ts +548 -0
  79. package/src/routes.ts +287 -0
  80. package/src/runtime-paths.ts +179 -0
  81. package/src/schema/config.ts +133 -0
  82. package/src/schema/state.ts +376 -0
  83. package/src/spawn.ts +103 -0
  84. package/src/state.ts +265 -0
  85. package/src/task-manager.ts +657 -0
  86. package/src/task-views.ts +222 -0
  87. package/src/text.test.ts +689 -0
  88. package/src/text.ts +683 -0
  89. package/src/wechat-preview/assets/fonts/SweiCurveLegCJKsc-Regular.woff +0 -0
  90. package/src/wechat-preview/browser/editor-export.css +241 -0
  91. package/src/wechat-preview/browser/editor-export.ts +103 -0
  92. package/src/wechat-preview/frontmatter-handler.ts +73 -0
  93. package/src/wechat-preview/index.test.ts +154 -0
  94. package/src/wechat-preview/index.ts +294 -0
  95. package/src/wechat-preview/themes.ts +141 -0
  96. package/src/wechat-preview/wechat-formatter.test.ts +288 -0
  97. package/src/wechat-preview/wechat-formatter.ts +1506 -0
  98. package/src/wechat-preview.test.ts +73 -0
  99. package/src/workflow-materials.test.ts +225 -0
  100. package/src/workflow-materials.ts +217 -0
  101. package/src/workflow.test.ts +2087 -0
  102. package/dist/assets/imgx/assets/icons/avatar_jinx_cartoon.jpg +0 -0
  103. package/dist/assets/imgx/assets/icons/fishbone-logo-square.png +0 -0
  104. package/dist/assets/imgx/assets/icons/fishbone-logo.jpg +0 -0
  105. package/dist/assets/imgx/assets/icons/fishbone-logo.png +0 -0
  106. package/dist/assets/imgx/assets/icons/openclaw-logo.svg +0 -22
  107. package/dist/assets/imgx/assets/icons/zzclub-logo-black.jpg +0 -0
  108. package/dist/assets/imgx/assets/icons/zzclub-logo-gray.svg +0 -25
  109. package/dist/cli.js +0 -432
  110. package/dist/node_modules/@chenglou/pretext/dist/analysis.d.ts +0 -33
  111. package/dist/node_modules/@chenglou/pretext/dist/analysis.js +0 -1063
  112. package/dist/node_modules/@chenglou/pretext/dist/bidi.d.ts +0 -1
  113. package/dist/node_modules/@chenglou/pretext/dist/bidi.js +0 -175
  114. package/dist/node_modules/@chenglou/pretext/dist/generated/bidi-data.d.ts +0 -4
  115. package/dist/node_modules/@chenglou/pretext/dist/generated/bidi-data.js +0 -979
  116. package/dist/node_modules/@chenglou/pretext/dist/layout.d.ts +0 -70
  117. package/dist/node_modules/@chenglou/pretext/dist/layout.js +0 -496
  118. package/dist/node_modules/@chenglou/pretext/dist/line-break.d.ts +0 -36
  119. package/dist/node_modules/@chenglou/pretext/dist/line-break.js +0 -820
  120. package/dist/node_modules/@chenglou/pretext/dist/measurement.d.ts +0 -28
  121. package/dist/node_modules/@chenglou/pretext/dist/measurement.js +0 -219
  122. package/dist/node_modules/@chenglou/pretext/dist/rich-inline.d.ts +0 -51
  123. package/dist/node_modules/@chenglou/pretext/dist/rich-inline.js +0 -401
  124. /package/{dist/assets → src}/imgx/assets/browser/obstacle-flow.d.ts +0 -0
  125. /package/{dist/assets → src}/imgx/assets/browser/obstacle-flow.js +0 -0
  126. /package/{dist/assets → src}/imgx/assets/icons/clover.svg +0 -0
  127. /package/{dist/assets → src}/imgx/assets/templates/ascii-portrait-3-4.html +0 -0
  128. /package/{dist/assets → src}/imgx/assets/templates/ascii-portrait-tile.html +0 -0
  129. /package/{dist/assets → src}/imgx/assets/templates/tips-3-4.html +0 -0
  130. /package/{dist/assets → src}/imgx/assets/templates/x-like-posts.html +0 -0
  131. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/.vite/manifest.json +0 -0
  132. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  133. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  134. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  135. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  136. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  137. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  138. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  139. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  140. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  141. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  142. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  143. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  144. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  145. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  146. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  147. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  148. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  149. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  150. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  151. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  152. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  153. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  154. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  155. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  156. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  157. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  158. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  159. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  160. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  161. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  162. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  163. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  164. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  165. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  166. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  167. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  168. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  169. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  170. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  171. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  172. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  173. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  174. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  175. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  176. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  177. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  178. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  179. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  180. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  181. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  182. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  183. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  184. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  185. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  186. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  187. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  188. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  189. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  190. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  191. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/SweiCurveLegCJKsc-Regular-B3Ut5bwH.woff +0 -0
  192. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/style-BYNXnUvc.css +0 -0
  193. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/editor-export.js +0 -0
  194. /package/{dist/assets → src}/wechat-preview/assets/templates/export-shell.html +0 -0
@@ -0,0 +1,81 @@
1
+ export type PosterBlock = {
2
+ text: string;
3
+ highlight: boolean;
4
+ };
5
+
6
+ export type TipItem = {
7
+ title: string;
8
+ description: string;
9
+ };
10
+
11
+ export type PosterConfig = {
12
+ blocks: PosterBlock[];
13
+ highlightWords: string[];
14
+ highlightColor: string;
15
+ tips: TipItem[];
16
+ };
17
+
18
+ type PosterInput = {
19
+ text: string;
20
+ line1: string;
21
+ line2: string;
22
+ line3: string;
23
+ hl1: boolean;
24
+ hl2: boolean;
25
+ hl3: boolean;
26
+ highlightWords: string;
27
+ highlightColor: string;
28
+ tips: TipItem[];
29
+ };
30
+
31
+ function buildBlocksFromText(text: string, lineHighlights: boolean[]): PosterBlock[] {
32
+ return text
33
+ .split(/\r?\n/)
34
+ .map(part => part.trim())
35
+ .filter(Boolean)
36
+ .map((part, index) => ({
37
+ text: part,
38
+ highlight: lineHighlights[index] ?? false,
39
+ }));
40
+ }
41
+
42
+ export function buildPosterConfig(input: PosterInput): PosterConfig {
43
+ const lineHighlights = [input.hl1, input.hl2, input.hl3];
44
+ const blocks =
45
+ input.text.trim().length > 0
46
+ ? buildBlocksFromText(input.text, lineHighlights)
47
+ : [input.line1, input.line2, input.line3]
48
+ .map((part, index) => ({
49
+ text: part.trim(),
50
+ highlight: lineHighlights[index] ?? false,
51
+ }))
52
+ .filter(block => block.text.length > 0);
53
+
54
+ const fallbackBlocks =
55
+ blocks.length > 0
56
+ ? blocks
57
+ : [
58
+ {
59
+ text: "",
60
+ highlight: false,
61
+ },
62
+ ];
63
+
64
+ return {
65
+ blocks: fallbackBlocks,
66
+ highlightWords: input.highlightWords
67
+ .split(",")
68
+ .map(word => word.trim())
69
+ .filter(Boolean),
70
+ highlightColor: input.highlightColor,
71
+ tips: input.tips,
72
+ };
73
+ }
74
+
75
+ export function serializePosterConfig(config: PosterConfig): string {
76
+ return JSON.stringify(config)
77
+ .replaceAll("<", "\\u003c")
78
+ .replaceAll(">", "\\u003e")
79
+ .replaceAll("&", "\\u0026")
80
+ .replaceAll("</script", "<\\/script");
81
+ }
@@ -0,0 +1,14 @@
1
+ export {
2
+ clearCache,
3
+ layout,
4
+ layoutNextLine,
5
+ layoutNextLineRange,
6
+ layoutWithLines,
7
+ materializeLineRange,
8
+ measureLineStats,
9
+ measureNaturalWidth,
10
+ prepare,
11
+ prepareWithSegments,
12
+ setLocale,
13
+ walkLineRanges,
14
+ } from "@chenglou/pretext";
@@ -0,0 +1,61 @@
1
+ import type { Canvas } from "@napi-rs/canvas";
2
+ import { FONTS_DIR } from "./runtime";
3
+
4
+ let initialized = false;
5
+
6
+ // Lazy-loaded canvas module — avoids crash at import time when @napi-rs/canvas is missing.
7
+ let canvasMod: { Canvas: typeof Canvas; GlobalFonts: typeof import("@napi-rs/canvas").GlobalFonts; createCanvas: typeof import("@napi-rs/canvas").createCanvas } | null = null;
8
+
9
+ function getCanvas() {
10
+ if (!canvasMod) {
11
+ try {
12
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
13
+ canvasMod = require("@napi-rs/canvas");
14
+ } catch {
15
+ throw new Error(
16
+ "@napi-rs/canvas not installed. Required for image rendering.\n" +
17
+ "Install it with:\n" +
18
+ " npm install @napi-rs/canvas\n" +
19
+ " # or: bun add @napi-rs/canvas",
20
+ );
21
+ }
22
+ }
23
+ return canvasMod!;
24
+ }
25
+
26
+ class BunOffscreenCanvas {
27
+ width: number;
28
+ height: number;
29
+ #canvas: Canvas;
30
+
31
+ constructor(width: number, height: number) {
32
+ this.width = width;
33
+ this.height = height;
34
+ this.#canvas = getCanvas().createCanvas(width, height);
35
+ }
36
+
37
+ getContext(type: string): ReturnType<Canvas["getContext"]> | null {
38
+ if (type !== "2d") {
39
+ return null;
40
+ }
41
+ return this.#canvas.getContext("2d");
42
+ }
43
+ }
44
+
45
+ function registerLongformFonts(): void {
46
+ const { GlobalFonts } = getCanvas();
47
+ GlobalFonts.registerFromPath(`${FONTS_DIR}/AlimamaShuHeiTi-Bold.ttf`, "AlimamaShuHeiTi");
48
+ GlobalFonts.registerFromPath(`${FONTS_DIR}/LXGWNeoZhiSongPlus.ttf`, "LXGWNeoZhiSongPlus");
49
+ GlobalFonts.registerFromPath(`${FONTS_DIR}/LXGWWenKai-Regular.ttf`, "LXGWWenKai");
50
+ }
51
+
52
+ export function ensurePretextRuntime(): void {
53
+ if (initialized) {
54
+ return;
55
+ }
56
+ if (typeof globalThis.OffscreenCanvas === "undefined") {
57
+ globalThis.OffscreenCanvas = BunOffscreenCanvas as unknown as typeof OffscreenCanvas;
58
+ }
59
+ registerLongformFonts();
60
+ initialized = true;
61
+ }
@@ -0,0 +1,59 @@
1
+ # ascii-portrait-3-4 模板规范
2
+
3
+ 比例:3:4 | 尺寸:900×1200 | 用途:把任意头像转换成多字符亮度映射海报
4
+
5
+ ## 特点
6
+
7
+ - 输入一张头像,自动采样颜色与亮度
8
+ - 使用多字符集合重建轮廓,不是单一字符硬拼
9
+ - 保留头像的综合色气质,而不是只做黑白 ASCII
10
+ - 适合实验性头像海报、人物封面、账号视觉页
11
+
12
+ ## 适用场景
13
+
14
+ - 用户要“字符头像海报”或“ASCII 人像海报”
15
+ - 用户给出一张头像,希望转成更有风格的视觉页
16
+ - 需要把头像做成独立海报,而不是插入长文正文
17
+
18
+ ## 推荐命令
19
+
20
+ ```bash
21
+ bun ./scripts/render-ascii-portrait.ts \
22
+ --avatar /absolute/path/to/avatar.jpg \
23
+ --title "Character Portrait" \
24
+ --out tmp/ascii-portrait.png
25
+ ```
26
+
27
+ 需要更细腻的人像时:
28
+
29
+ ```bash
30
+ bun ./scripts/render-ascii-portrait.ts \
31
+ --avatar /absolute/path/to/avatar.jpg \
32
+ --title "Character Portrait" \
33
+ --columns 88 \
34
+ --chars "@#X$%WM*+=-" \
35
+ --out tmp/ascii-portrait-detail.png
36
+ ```
37
+
38
+ ## 参数说明
39
+
40
+ - `--avatar`:必填,输入头像
41
+ -- `--title`:海报标题
42
+ - `--out`:必填,输出 PNG 路径
43
+ - `--chars`:字符梯度;`ascii-portrait-3-4` 默认 `@#X$%WM*+=-`,仅使用更厚实的大写字符和符号
44
+ - `--columns`:横向采样列数,越大越细;默认 `auto`,按原图尺寸自动计算,并钳制在 `54–84`
45
+ - `--font-size`:字符字号;默认 `auto`,根据列数自动推导
46
+ - `--line-height`:字符行高;默认 `auto`,根据字号自动推导
47
+ - `--bg`:背景色;默认 `auto`,会基于头像中心区与边缘区的亮度差,自动在深底/浅底之间选择
48
+
49
+ - `--footer`:底部文字,默认 `公众号 · 早早集市`
50
+ - `--icon`:底部图标路径
51
+ - `--template`:模板名称,支持 `ascii-portrait-3-4`(默认)和 `ascii-portrait-tile`
52
+ - `--width`:画布宽度,默认 `900`
53
+ - `--height`:画布高度,默认 `1200`
54
+
55
+ ## 使用建议
56
+
57
+ - 对比度强、人物和背景分离明显的头像效果最好
58
+ - 想更抽象:减少 `--columns`
59
+ - 想更还原:增加 `--columns` 并使用更长的 `--chars`
@@ -0,0 +1,173 @@
1
+ # longform-3-4 模板规范
2
+
3
+ 比例:3:4 | 尺寸:900×1200 | 用途:长文正文卡片,适合非封面页、长段落、阅读型内容
4
+
5
+ ## 特点
6
+
7
+ - 复用 `poster-3-4` 的顶部 logo 和底部 watermark 结构
8
+ - 正文使用 `LXGWNeoZhiSongPlus.ttf`
9
+ - 行高更舒展,适合阅读,不追求封面式冲击力
10
+ - 支持 markdown 正文
11
+ - 支持正文中的正文插图(内容插图),文字会自动绕排
12
+ - 障碍物流排逻辑来自共享浏览器模块 `assets/browser/obstacle-flow.js`
13
+ - 分页基于真实排版高度,而不是字符数估算
14
+ - 分页测量优先直接在进程内执行 `pretext`,Chrome 只负责最终截图
15
+ - 最终截图阶段会补偿 headless Chrome 的 viewport inset,再裁回目标尺寸,避免 footer 在截图时被底部裁掉
16
+ - 当存在显式 `page_specs` 时,支持 spec-driven 分页:固定页序列、按页图文占比近似求解
17
+ - 样式层可切换主题,便于同模板服务不同账号
18
+
19
+ ## 适用场景
20
+
21
+ - 用户要“文章分页图”或“阅读内页”
22
+ - 用户给的是长文,而不是一句标题
23
+ - 正文里需要插图、caption、ASCII 头像块这类正文插图
24
+ - 同一模板要服务多个账号,只换主题和图文布局风格
25
+
26
+ ## 调用方式
27
+
28
+ 单页长文:
29
+
30
+ ```bash
31
+ bun ./scripts/render-article.ts \
32
+ --template longform-3-4 \
33
+ --theme linen-news \
34
+ --title "文章标题" \
35
+ --text "这里是一页长文正文" \
36
+ --out tmp/longform.png
37
+ ```
38
+
39
+ 真实分页,多图导出:
40
+
41
+ ```bash
42
+ bun ./scripts/render-article.ts \
43
+ --template longform-3-4 \
44
+ --theme paper-sage \
45
+ --title "文章标题" \
46
+ --text-file /absolute/path/to/article.md \
47
+ --out-dir tmp/longform-pages
48
+ ```
49
+
50
+ 图文混排,正文插图绕排:
51
+
52
+ ```bash
53
+ bun ./scripts/render-article.ts \
54
+ --template longform-3-4 \
55
+ --theme linen-news \
56
+ --text-file /absolute/path/to/article.md \
57
+ --body-image /absolute/path/to/image.jpg \
58
+ --image-side right \
59
+ --image-layout editorial-float \
60
+ --image-caption "配图说明" \
61
+ --out tmp/longform-mixed.png
62
+ ```
63
+
64
+ 把 ASCII 头像块作为正文插图插入正文:
65
+
66
+ ```bash
67
+ bun ./scripts/render-article.ts \
68
+ --template longform-3-4 \
69
+ --text-file /absolute/path/to/article.md \
70
+ --body-ascii-portrait /absolute/path/to/avatar.jpg \
71
+ --ascii-side left \
72
+ --ascii-layout mid-left \
73
+ --ascii-caption "作者头像 ASCII 版" \
74
+ --out tmp/longform-ascii.png
75
+ ```
76
+
77
+ ## 正文插图布局预设(`--image-layout`)
78
+
79
+ | 预设值 | 别名 | 视觉效果 | 适用场景 |
80
+ |--------|------|----------|----------|
81
+ | `auto` | `default` | 按 `--image-side` 固定边,纵向等距排列 | 1 张插图,简单明确 |
82
+ | `staggered` | `split-dual` | 奇数张靠指定边,偶数张自动翻转对侧,纵向拉开间距 | **多张插图首选**,左右交替有节奏感 |
83
+ | `editorial` | `editorial-float` | 同侧排列但每张有额外竖向错位,版式感强 | 2–3 张,杂志排版风格 |
84
+ | `corner-soft` | — | 靠近顶部角落,纵向紧凑叠放 | 插图作角落装饰,正文为主 |
85
+ | `mid-left` | — | 页面中段靠左,纵向等距 | 明确放左侧中部 |
86
+ | `mid-right` | — | 页面中段靠右,纵向等距 | 明确放右侧中部 |
87
+
88
+ 默认值:未指定时为 `auto`。pipeline 模式下 orchestrator 统一默认使用 `staggered`(适合 2 张及以上)。
89
+
90
+ ## 参数说明
91
+
92
+ 与 `render-article.ts` 的通用参数基本一致,额外约定如下:
93
+
94
+ - `--template longform-3-4`:启用 LXGW 阅读版模板
95
+ - `--theme`:主题名,默认 `paper-sage`,支持 `paper-sage` / `linen-news`
96
+ - `--title`:当前模板不在正文区显示标题,但参数仍保留给脚本统一接口
97
+ - `--text` / `--text-file`:正文内容,支持有限的块级 markdown 子集
98
+ - `--out`:输出单张图;如果正文超长,会按指定 `--page-num` 或默认第 1 页导出
99
+ - `--out-dir`:输出多张分页图,文件名形如 `article-01.png`
100
+ - `--body-image`:可重复传入,多张正文插图路径
101
+ - `--image-side`:可重复传入,支持 `left` / `right`
102
+ - `--image-layout`:自动布局预设,默认 `auto`,支持 `auto` / `staggered` / `editorial`
103
+ 也支持更偏风格名的别名:`corner-soft` / `mid-left` / `mid-right` / `split-dual` / `editorial-float`
104
+ - `--image-x` / `--image-y` / `--image-width` / `--image-height`:可重复传入,覆盖每张图片的位置和尺寸
105
+ 默认图片尺寸为 210×210
106
+ - `--image-caption`:可重复传入,给对应图片添加 caption
107
+ - `--body-ascii-portrait`:把 ASCII 头像块作为障碍物组合进正文
108
+ - `--ascii-side` / `--ascii-layout` / `--ascii-width` / `--ascii-height` / `--ascii-caption`:控制 ASCII 障碍物的位置与尺寸
109
+ - `--ascii-bg`:ASCII 块背景色
110
+ - `--ascii-chars`:字符梯度,默认 `@#W$9876543210?!abc;:+=-,._ `
111
+ - `--ascii-columns`:ASCII 横向采样列数,默认 `34`
112
+ - `--ascii-x` / `--ascii-y`:覆盖 ASCII 障碍物位置坐标
113
+ - `--page-width` / `--page-height`:覆盖页面尺寸
114
+ - `--body-padding-x` / `--body-padding-y`:覆盖正文外边距
115
+ - `--logo-size` / `--logo-gap`:覆盖 header 几何
116
+ - `--footer-height` / `--footer-margin-top`:覆盖 footer 预留高度
117
+ - `--content-width` / `--content-height`:显式指定正文内容区
118
+ - `--content-bottom-gap`:控制内容区底部保留空间
119
+ - `#` / `##` / `>` / `-`:会解析成小标题、引用、列表项,并进入同一套正文插图流排系统;当前不是完整 markdown 渲染器
120
+
121
+ ## 内容区几何
122
+
123
+ `longform-3-4` 现在把内容区当成“可推导区域”而不是固定常量:
124
+
125
+ - 默认内容宽度:`pageWidth - bodyPaddingX * 2`
126
+ - 默认内容高度:`pageHeight - bodyPaddingY * 2 - (logoSize + logoGap) - (footerHeight + footerMarginTop)`
127
+ - 如果调用方显式传了 `--content-width` / `--content-height`,会在可用范围内裁到该值
128
+
129
+ 这意味着同一个模板里,只要 header/footer/padding/page size 改了,分页算法也会跟着重算,不需要再去源码里找写死的内容区尺寸。
130
+
131
+ ## 分页图片消费逻辑
132
+
133
+ ## Spec 驱动分页
134
+
135
+ 当调用方传入 `--page-image-spec-file` 时,`longform-3-4` 不再只按“当前页塞满才翻页”处理,而是进入另一条版式路径:
136
+
137
+ - 有正文页标记时:
138
+ `【第一页】` / `【第二页】` / `【Page 1】` / `【Page 2】` 会成为硬分页锚点
139
+ - 没有正文页标记时:
140
+ 会按段落 block 顺序,把正文自动分配到显式页序列里
141
+ - 每页图片会做一轮页内缩放搜索,尽量让“文字 + 图片”占到内容区约 `target_fill_ratio`
142
+ - 顶层 `target_fill_ratio` 默认 `0.8`
143
+ - `page_specs[].target_fill_ratio` 可以覆盖单页目标
144
+
145
+ 这条路径适合:
146
+
147
+ - 已经知道要分几页
148
+ - 每页已经知道有哪些插图
149
+ - 希望某些文字固定在指定页里,即使该页文字不算很多
150
+
151
+ **分页图片消费逻辑(`--body-image` 在多页中的分配):**
152
+
153
+ 当正文超过一页时,`render-article.ts` 通过 `obstacle-flow.js` 的 `paginateBlocks()` 将正文和图片分配到各页:
154
+
155
+ - **消费方式**: 顺序切片(sequential slice),不是全局障碍物
156
+ - **每页上限**: `pageImageLimit = 2`(当前硬编码)
157
+ - **分配规则**: `bodyImages.slice(imageIndex, imageIndex + pageImageLimit)` — 第 1 页取前 2 张,第 2 页取接下来 2 张,依此类推
158
+ - **图片顺序**: 与 `--body-image` 参数的传入顺序一致
159
+ - **超出页数**: 若图片数 > 页数 × 2,多余图片不渲染(不报错)
160
+ - **不足**: 若某页分配不到图片,该页无插图,只渲染文字
161
+
162
+ > 这意味着传入 4 张 `--body-image`,前 2 张出现在第 1 页,后 2 张出现在第 2 页。
163
+ > 若只有 1 页但传了 3 张图,只有前 2 张会渲染。
164
+
165
+ ## 注意事项
166
+
167
+ - 正文中若含 `插图N` / `配图N` 占位标记,调用本脚本前必须先由 orchestrator 剔除,否则标记文字会直接渲染进卡片
168
+ - 适合正文、随笔、文章分页内页、说明卡
169
+ - 不适合大标题封面
170
+ - 若是封面或强视觉标题,优先使用 `poster-3-4`
171
+ - 需要图文混排时,优先使用 `--body-image` 配合 `--image-side left/right`
172
+ - 多账号复用时,优先固定 `--theme` + `--image-layout` 的组合,而不是每次手调坐标
173
+ - 真正的长文发布优先使用 `--out-dir`,让模板自动产出多张连续图片
@@ -0,0 +1,87 @@
1
+ # poster-3-4 模板规范
2
+
3
+ 比例:3:4 | 尺寸:900×1200 | 用途:小绿书 / 公众号 / 小红书通用封面
4
+
5
+
6
+ ## 渲染命令
7
+
8
+ ```bash
9
+ bun ./scripts/render-card.ts \
10
+ --template poster-3-4 \
11
+ --out tmp/card.png \
12
+ --text "OpenClaw 有两层 model 配置" \
13
+ --highlight "#22a854" \
14
+ --highlight-words "OpenClaw,model" \
15
+ --bg "#e6f5ef" \
16
+ --footer "公众号 · 早早集市"
17
+ ```
18
+
19
+ 如果用户已经明确拆成三行,也可以直接按行控制:
20
+
21
+ ```bash
22
+ bun ./scripts/render-card.ts \
23
+ --template poster-3-4 \
24
+ --line1 "OpenClaw 有两层" \
25
+ --line2 "model 配置" \
26
+ --hl2 \
27
+ --out tmp/card-lines.png
28
+ ```
29
+
30
+ ## 参数说明
31
+
32
+ | 参数 | 默认值 | 说明 |
33
+ |------|--------|------|
34
+ | `--text` | 空 | 推荐入口。整段文案,自适应字号和自动断行 |
35
+ | `--line1` | 空 | 第一行文字 |
36
+ | `--line2` | 空 | 第二行文字(空则隐藏)|
37
+ | `--line3` | 空 | 第三行文字(空则隐藏)|
38
+ | `--hl1` | 关闭 | 整行高亮:第一行 |
39
+ | `--hl2` | 关闭 | 整行高亮:第二行 |
40
+ | `--hl3` | 关闭 | 整行高亮:第三行 |
41
+ | `--highlight-words` | 空 | 按子串高亮,逗号分隔(子串匹配,非单词边界),如 `OpenClaw,GPT-5.4`(跨行生效)|
42
+ | `--highlight` | `#22a854` | 高亮/强调色 |
43
+ | `--bg` | `#e6f5ef` | 背景色 |
44
+ | `--footer` | `公众号 · 早早集市` | 底部文字 |
45
+ | `--icon` | 自动判断 | 顶部图标路径,不传则按内容自动选 |
46
+ | `--template` | `poster-3-4` | 模板名称 |
47
+ | `--out` | — | 必填,输出 PNG 路径 |
48
+ | `--fallback-icon` | — | 备用图标路径,`--icon` 判空时使用 |
49
+
50
+ ## 高亮文字处理规则
51
+
52
+ **两种高亮方式,按需选择:**
53
+
54
+ 当使用 `--text` 时,优先使用 `--highlight-words`。当使用 `--line1/2/3` 时,`--hl1/2/3` 仍然有效。
55
+
56
+ ### 1. 整行高亮(`--hl1` / `--hl2` / `--hl3`)
57
+ 整行文字渲染为高亮色。适合"某一行是关键句"的场景。
58
+
59
+ > 示例:第二行要高亮
60
+ > → `--line1 "GPT-5.4 发布了" --line2 "能控电脑" --hl2`
61
+
62
+ ### 2. 按词高亮(`--highlight-words`)
63
+ 在任意行中,将指定词语渲染为高亮色,其余文字保持黑色。适合"某几个关键词"的场景。
64
+
65
+ > 示例:高亮 OpenClaw 和 GPT-5.4
66
+ > → `--line1 "GPT-5.4 最适合" --line2 "OpenClaw 使用" --highlight-words "GPT-5.4,OpenClaw"`
67
+
68
+ **优先级规则(有歧义时):**
69
+ - 用户说「高亮第X行」→ 用 `--hlX`
70
+ - 用户说「高亮某个词/某几个词」→ 用 `--highlight-words`
71
+ - 两者可以同时使用
72
+
73
+
74
+
75
+ | 位置 | 最多字数 | 说明 |
76
+ |------|---------|------|
77
+ | 每行(line1/2/3) | **6~7 个汉字** / **10~12 个英文字符** | 字号 108px,可用宽 720px |
78
+ | 三行合计 | **≤ 20 字** | 超出则横向溢出,无法使用 |
79
+
80
+ > 用户文案超出时,先帮忙拆分/缩写到上限内,再渲染,不要直接塞入模板。
81
+
82
+ ## 配图选取原则
83
+
84
+ | 条件 | 使用图标 | 文件 |
85
+ |------|---------|------|
86
+ | 默认 | 通用 logo | `assets/icons/logo.svg` |
87
+ | 自定义 | 通过 `--icon` 参数指定 | 任意路径 |
@@ -0,0 +1,69 @@
1
+ # tips-3-4 模板规范
2
+
3
+ 比例:`3:4` | 尺寸:900×1200 | 用途:小红书风格结构化要点卡
4
+
5
+ ## 适用场景
6
+
7
+ - 用户说"N 个方法 / 技巧 / 步骤 / 要点"
8
+ - 用户说"小红书要点卡 / 干货清单"
9
+ - 内容为 3–5 条有标题(可选副标明)的并列技巧
10
+
11
+ ## 渲染命令
12
+
13
+ ```bash
14
+ bun ./scripts/render-card.ts \
15
+ --template tips-3-4 \
16
+ --text "写作变现的\n5 个核心技巧" \
17
+ --tip "选题精准::锁定高搜索量、低竞争度的细分话题" \
18
+ --tip "钩子开头::前 50 字决定是否被划走" \
19
+ --tip "结构清晰::小标题 + 短段落,移动端必须" \
20
+ --tip "引导互动::结尾提问或给出可执行行动" \
21
+ --tip "持续复盘::每月看数据,找爆文规律" \
22
+ --highlight "#22a854" \
23
+ --bg "#e6f5ef" \
24
+ --footer "公众号 · 早早集市" \
25
+ --out tmp/tips-card.png
26
+ ```
27
+
28
+ 如果 tip 只有标题,不带副标明,省略 `::` 后半部分:
29
+
30
+ ```bash
31
+ --tip "选题精准" \
32
+ --tip "钩子开头" \
33
+ ```
34
+
35
+ ## 参数说明
36
+
37
+ | 参数 | 默认值 | 说明 |
38
+ |------|--------|------|
39
+ | `--template` | `tips-3-4` | 模板名称 |
40
+ | `--out` | — | 必填,输出 PNG 路径 |
41
+ | `--text` | 空 | **必填**。标题文案,支持 `\n` 手动换行,pretext 自适应字号 |
42
+ | `--tip` | — | 可重复传入,每条格式 `标题::描述`,描述可省略 |
43
+ | `--hl1/hl2/hl3` | 关闭 | 整行高亮(作用于 `--text` 对应行) |
44
+ | `--highlight` | `#22a854` | 强调色,用于序号圆圈、badge 边框、高亮文字 |
45
+ | `--bg` | `#e6f5ef` | 背景色 |
46
+ | `--footer` | `公众号 · 早早集市` | 底部水印文字 |
47
+ | `--icon` | 默认 logo | 左上角圆形 icon |
48
+
49
+ ## 内容建议
50
+
51
+ | 位置 | 建议 |
52
+ |------|------|
53
+ | 标题 (`--text`) | 10–20 字,含数字最佳,如"5 个核心技巧" |
54
+ | tips 条数 | 3–5 条;超过 5 条会压缩间距,视觉密度过高 |
55
+ | 每条标题 | 4–10 字 |
56
+ | 每条描述 | 10–25 字,超长会换行,撑高 tip 区域 |
57
+
58
+ 标题由 pretext 引擎根据可用空间自动选择最大可用字号,tip 区域高度固定,两者之间通过两步测量动态分配空间。
59
+
60
+ ## 默认 icon
61
+
62
+ 默认使用 `assets/icons/logo.svg`。可通过 `--icon` 参数覆盖。
63
+
64
+ ## 使用规则
65
+
66
+ - 内容是「N 个 XX」列表型干货 → 用本模板
67
+ - 内容是金句 / 单标题封面 → 用 `poster-3-4`
68
+ - 内容是公众号文章封面图 → 用 `wechat-cover-split`
69
+ - **强规则:tips 条数不足 3 条时,改用 `poster-3-4` 做多行标题更合适**
@@ -0,0 +1,89 @@
1
+ # wechat-cover-split 模板规范
2
+
3
+ 比例:`335:100`(左 `2.35:1` + 右 `1:1`) | 尺寸:1340×400 | 用途:公众号文章封面图
4
+
5
+ > 该模板渲染时会额外调用 `ffmpeg` 做顶部精确裁切,以适配 Chrome 在短横幅截图下的视口偏差。
6
+
7
+ ## 适用场景
8
+
9
+ - 用户说"公众号文章封面图"
10
+ - 用户说"微信公众号头图 / 封面长图"
11
+ - 需要一张图同时切出左侧横图和右侧方图
12
+
13
+ ## 切图规则
14
+
15
+ 整张图由两部分组成:
16
+
17
+ - 左侧:`940×400`,比例 `2.35:1`,放标题文案
18
+ - 右侧:`400×400`,比例 `1:1`,放 icon
19
+
20
+ 如果业务端需要拆图使用:
21
+
22
+ - 左图:取左侧 `940×400`
23
+ - 右图:取右侧 `400×400`
24
+
25
+ ## 渲染命令
26
+
27
+ ```bash
28
+ bun ./scripts/render-card.ts \
29
+ --template wechat-cover-split \
30
+ --out tmp/wechat-cover.png \
31
+ --line1 "OpenAI 收购 Promptfoo" \
32
+ --line2 "意味着什么" \
33
+ --highlight "#22a854" \
34
+ --bg "#eef7f2" \
35
+ --footer "公众号 · 早早集市"
36
+ ```
37
+
38
+ 如果标题更长,优先继续走同一模板,而不是换模板:
39
+
40
+ ```bash
41
+ bun ./scripts/render-card.ts \
42
+ --template wechat-cover-split \
43
+ --text "这是一篇公众号文章的长标题,需要自动切分并保留高亮重点" \
44
+ --highlight-words "高亮重点" \
45
+ --out tmp/wechat-cover-long.png
46
+ ```
47
+
48
+ ## 参数说明
49
+
50
+ | 参数 | 默认值 | 说明 |
51
+ |------|--------|------|
52
+ | `--template` | `wechat-cover-split` | 模板名称 |
53
+ | `--out` | — | 必填,输出 PNG 路径 |
54
+ | `--text` | 空 | **推荐入口**。整段标题文案,自动切分为两行并自适应字号 |
55
+ | `--line1` | 空 | 第一行标题(已有拆分结果时使用) |
56
+ | `--line2` | 空 | 第二行标题(已有拆分结果时使用) |
57
+ | `--line3` | 空 | 预留;自动并入第二行,不单独显示 |
58
+ | `--hl1/hl2/hl3` | 关闭 | 整行高亮 |
59
+ | `--highlight-words` | 空 | 按词高亮,逗号分隔,跨行生效 |
60
+ | `--highlight` | `#22a854` | 强调色 |
61
+ | `--bg` | `#e6f5ef` | 背景色 |
62
+ | `--footer` | `公众号 · 早早集市` | 底部文字 |
63
+ | `--icon` | 默认 logo | 右侧 1:1 区域 icon |
64
+
65
+ ## 默认 icon
66
+
67
+ 默认使用 `assets/icons/logo.svg`。如果用户指定其他品牌图标,再显式传 `--icon` 覆盖。
68
+
69
+ ## 字数建议
70
+
71
+ 模板使用 pretext 自适应字号,超长文案会自动缩小字体兜底,不会溢出。但视觉质量随字数增加而下降,建议控制在合理范围:
72
+
73
+ | 位置 | 建议字数 | 说明 |
74
+ |------|---------|------|
75
+ | 单行 | 12~18 个汉字 | 优先单行呈现,视觉冲击力最强 |
76
+ | 总行数 | ≤ 2 行 | 超出两行字号会明显缩小,横幅视觉受损 |
77
+ | 总字数 | ≤ 28 个汉字 | 超出建议先缩写或拆标题,而不是硬塞 |
78
+
79
+ 优先做法:
80
+
81
+ 1. 先抽标题主干
82
+ 2. 优先单行,必要时拆成 2 行
83
+ 3. 关键词可用整行或按词高亮
84
+
85
+ ## 使用规则
86
+
87
+ - 用户明确说"公众号文章封面图"时,优先用本模板
88
+ - 用户只是要常规封面 / 金句图,仍使用 `poster-3-4`
89
+ - **强规则:公众号文章封面图必须至少有一个高亮词**(`--highlight-words` 或 `--hl1/hl2/hl3`),不可省略