@mevdragon/vidfarm-devcli 0.2.8 → 0.2.10

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 (98) hide show
  1. package/.env.example +9 -0
  2. package/GETTING_STARTED.developers.md +1 -26
  3. package/README.md +3 -0
  4. package/SKILL.developer.md +419 -8
  5. package/auto-create-templates/AUTO_CREATE_TEMPLATES.md +475 -0
  6. package/auto-create-templates/extractor-system-prompt.md +113 -0
  7. package/auto-create-templates/production-graph.schema.json +276 -0
  8. package/auto-create-templates/runbook.md +140 -0
  9. package/auto-create-templates/template-plan.schema.json +230 -0
  10. package/auto-create-templates/template-planner-prompt.md +75 -0
  11. package/dist/src/account-pages-legacy.js +9064 -0
  12. package/dist/src/account-pages.js +41 -620
  13. package/dist/src/app.js +6627 -244
  14. package/dist/src/cli.js +386 -26
  15. package/dist/src/config.js +12 -2
  16. package/dist/src/context.js +35 -35
  17. package/dist/src/db.js +1016 -53
  18. package/dist/src/dev-app-legacy.js +693 -0
  19. package/dist/src/dev-app.js +4 -904
  20. package/dist/src/domain.js +1 -1
  21. package/dist/src/editor-chat-history.js +72 -0
  22. package/dist/src/editor-chat.js +202 -0
  23. package/dist/src/frontend/flockposter-cache-store.js +116 -0
  24. package/dist/src/frontend/homepage-client.js +114 -0
  25. package/dist/src/frontend/homepage-shared.js +3 -0
  26. package/dist/src/frontend/homepage-store.js +27 -0
  27. package/dist/src/frontend/homepage-view.js +147 -0
  28. package/dist/src/frontend/page-runtime-client.js +100 -0
  29. package/dist/src/frontend/page-runtime-store.js +8 -0
  30. package/dist/src/frontend/template-editor-chat.js +2261 -0
  31. package/dist/src/homepage.js +695 -371
  32. package/dist/src/lib/template-style-options.js +95 -15
  33. package/dist/src/page-runtime.js +1 -0
  34. package/dist/src/page-shell.js +941 -0
  35. package/dist/src/primitive-context.js +163 -0
  36. package/dist/src/primitive-registry.js +340 -0
  37. package/dist/src/primitive-sdk.js +3 -0
  38. package/dist/src/primitives/remotion/html-image.js +28 -0
  39. package/dist/src/react-page-shell.js +34 -0
  40. package/dist/src/ready-post-schedule-component.js +1514 -0
  41. package/dist/src/registry.js +36 -17
  42. package/dist/src/runtime.js +6 -1
  43. package/dist/src/services/api-call-history.js +245 -0
  44. package/dist/src/services/auth.js +25 -3
  45. package/dist/src/services/billing.js +248 -9
  46. package/dist/src/services/chat-threads.js +88 -0
  47. package/dist/src/services/job-logs.js +14 -7
  48. package/dist/src/services/jobs.js +13 -2
  49. package/dist/src/services/providers.js +552 -133
  50. package/dist/src/services/rate-limits.js +236 -0
  51. package/dist/src/services/remotion.js +143 -16
  52. package/dist/src/services/storage.js +10 -8
  53. package/dist/src/services/template-certification.js +11 -2
  54. package/dist/src/services/template-loader.js +3 -1
  55. package/dist/src/services/template-sources.js +68 -3
  56. package/dist/src/template-editor-pages.js +2309 -0
  57. package/dist/src/template-editor-shell.js +1507 -0
  58. package/dist/src/worker.js +120 -22
  59. package/package.json +27 -4
  60. package/public/assets/homepage-app.js +54 -0
  61. package/public/assets/homepage-client-app.js +54 -0
  62. package/public/assets/page-runtime-client-app.js +68 -0
  63. package/dist/templates/vidfarm_template_0000/src/lib/images.js +0 -202
  64. package/dist/templates/vidfarm_template_0000/src/remotion/Root.js +0 -34
  65. package/dist/templates/vidfarm_template_0000/src/remotion/index.js +0 -3
  66. package/dist/templates/vidfarm_template_0000/src/sdk.js +0 -3
  67. package/dist/templates/vidfarm_template_0000/src/style-options.js +0 -51
  68. package/dist/templates/vidfarm_template_0000/src/template-dna.js +0 -9
  69. package/dist/templates/vidfarm_template_0000/src/template.js +0 -1447
  70. package/templates/vidfarm_template_0000/README.md +0 -100
  71. package/templates/vidfarm_template_0000/SKILL.md +0 -225
  72. package/templates/vidfarm_template_0000/assets/Abel-Regular.ttf +0 -0
  73. package/templates/vidfarm_template_0000/assets/DMSerifDisplay-Regular.ttf +0 -0
  74. package/templates/vidfarm_template_0000/assets/Montserrat[wght].ttf +0 -0
  75. package/templates/vidfarm_template_0000/assets/SourceCodePro[wght].ttf +0 -0
  76. package/templates/vidfarm_template_0000/assets/TikTokSans-SemiBold.ttf +0 -0
  77. package/templates/vidfarm_template_0000/assets/Yesteryear-Regular.ttf +0 -0
  78. package/templates/vidfarm_template_0000/composition.json +0 -11
  79. package/templates/vidfarm_template_0000/package.json +0 -27
  80. package/templates/vidfarm_template_0000/research/preview/.gitkeep +0 -1
  81. package/templates/vidfarm_template_0000/research/source_notes.md +0 -7
  82. package/templates/vidfarm_template_0000/src/lib/images.js +0 -202
  83. package/templates/vidfarm_template_0000/src/lib/images.ts +0 -241
  84. package/templates/vidfarm_template_0000/src/remotion/Root.js +0 -33
  85. package/templates/vidfarm_template_0000/src/remotion/Root.tsx +0 -75
  86. package/templates/vidfarm_template_0000/src/remotion/index.js +0 -3
  87. package/templates/vidfarm_template_0000/src/remotion/index.tsx +0 -4
  88. package/templates/vidfarm_template_0000/src/sdk.js +0 -3
  89. package/templates/vidfarm_template_0000/src/sdk.ts +0 -140
  90. package/templates/vidfarm_template_0000/src/style-options.js +0 -51
  91. package/templates/vidfarm_template_0000/src/style-options.ts +0 -60
  92. package/templates/vidfarm_template_0000/src/template-dna.js +0 -9
  93. package/templates/vidfarm_template_0000/src/template-dna.ts +0 -15
  94. package/templates/vidfarm_template_0000/src/template.js +0 -1447
  95. package/templates/vidfarm_template_0000/src/template.ts +0 -2049
  96. package/templates/vidfarm_template_0000/template.config.json +0 -21
  97. package/templates/vidfarm_template_0000/tmp/solobacterium-moorei-slideshow.request.json +0 -31
  98. package/templates/vidfarm_template_0000/tsconfig.json +0 -19
@@ -1,100 +0,0 @@
1
- # vidfarm_template_0000
2
-
3
- Standalone GitHub-distributable package for the Vidfarm `template_0000` starter template.
4
-
5
- This package exists to give third-party developers a concrete example of the Vidfarm template contract.
6
-
7
- Developers should treat this repo as an authoring and review unit.
8
- The canonical template implementation lives in `src/template.ts`; the build emits `src/template.js` for runtime loading.
9
-
10
- When a template is ready, it is handed off for Vidfarm admin review and approval before it becomes available on the hosted platform.
11
-
12
- Every new template should keep its source-format research checked in:
13
-
14
- - `research/source_notes.md` for notes on the original winning format
15
- - `research/preview/` for the screenshots or source video the DNA analyzers inspect
16
- - `src/template-dna.ts` for the generated viral and visual DNA that feed the published template metadata
17
-
18
- Production integration settings are checked into `template.config.json`. AI agents and developers should treat that file as the source of truth for:
19
-
20
- - project identity
21
- - `template_id` as the self-issued UUIDv4 platform identifier
22
- - `slug_id` as the human-readable stable slug
23
- - the template module path, which should be the TypeScript source entrypoint `src/template.ts`
24
- - the local Remotion composition settings used by the starter template
25
-
26
- ## Required template contract
27
-
28
- Every new template copied from `template_0000` should keep the current required `defineTemplate(...)` contract intact.
29
-
30
- - `id`, `slugId`, and `version`
31
- - `about.title` and `about.description`
32
- - `about.viral_dna`, `about.visual_dna`, `about.preview_media`, and `about.link_to_original`
33
- - `about.proposal_generator`, which teaches a director or agent how to turn business context into template-native post proposals
34
- - `about.sample_prompts`, which shows multiple concrete high-signal prompts for the template
35
- - `configSchema`
36
- - at least one operation with a `smokeTestPayload`
37
- - a matching workflow implementation in `jobs`
38
- - a template-local `SKILL.md`
39
-
40
- The starter already ships with example `proposal_generator` and `sample_prompts` values in `src/template.ts`. When you adapt this template, replace them with instructions and examples that fit the new format instead of leaving the starter language in place.
41
-
42
- ## Included
43
-
44
- - `SKILL.md` for customer AI-agent usage
45
- - `src/template.ts` for the Vidfarm platform contract
46
- - `src/remotion/*` for local video rendering
47
- - `src/lib/images.ts` for non-cropping portrait normalization
48
- - `composition.json` sample props for local render tests
49
-
50
- ## Template behavior
51
-
52
- `template_0000` is intentionally opinionated because later templates will likely start from it.
53
-
54
- - stage 1 operation: `create_slideshow`
55
- - stage 2 operation: `render_video`
56
- - `create_slideshow` accepts either legacy `[imagePrompt, caption]` or `[imagePrompt, caption, durationMs]` slide tuples, or structured slide objects with optional `image_prompt_attachments` and `duration_ms`
57
- - `create_slideshow` also accepts optional `meta_details_prompt` guidance and returns generated `title`, `description`, `pinned_comment`, `location`, and `song` metadata plus the normalized metadata input args
58
- - slide generation is strict 9:16 with TikTok-safe reserved UI zones
59
- - finished slide images already include the text overlay
60
- - the video stage only accepts explicit `slides` input and sequences those finished slide images using per-slide `durationMs`
61
- - caption placement is center-biased and image-aware, using provider layout analysis first with a local fallback
62
- - caption styling is restricted to the checked-in starter option lists in `src/style-options.ts`
63
-
64
- The starter is optimized to:
65
-
66
- - avoid subject cut-off during portrait normalization
67
- - keep the right-side TikTok action rail clear
68
- - keep the top tabs area clear
69
- - keep the bottom caption/audio area clear
70
- - avoid fake full-width caption panels; if a text background is used, it renders as a tight line chip behind the words only
71
-
72
- ## Install
73
-
74
- ```bash
75
- npm install
76
- ```
77
-
78
- ## Validate
79
-
80
- ```bash
81
- npm run check
82
- ```
83
-
84
- ## Analyze Template DNA
85
-
86
- After you add source notes and preview media, run:
87
-
88
- ```bash
89
- vidfarm analyze-viral-dna --template-dir .
90
- vidfarm analyze-visual-dna --template-dir .
91
- ```
92
-
93
- If you scaffold a template with `vidfarm generate-template` and provide `--source-preview-dir`, the CLI will run both analyzers automatically unless `--skip-dna-analysis` is set.
94
-
95
- After DNA analysis, review and customize the remaining human-authored metadata before calling the template ready:
96
-
97
- - refresh `about.proposal_generator`
98
- - refresh `about.sample_prompts`
99
- - update `SKILL.md` to match the actual operations and payload shapes
100
- - run `vidfarm validate-template --template-id <slug-or-id>` and at least one real local job
@@ -1,225 +0,0 @@
1
- # Template 0000 Skill
2
-
3
- Use this template when the customer wants a fast vertical slideshow for TikTok-style distribution, built from a short list of scene prompts and exact overlay text.
4
-
5
- Operational rule:
6
-
7
- - template developers author and test this template locally
8
- - the hosted platform later runs the approved template behind the standard Vidfarm API wrapper
9
- - a Vidfarm admin reviews and approves the template before it is made available in production
10
-
11
- Inputs:
12
-
13
- - `slides`: either ordered tuples like `[imagePrompt, overlayText]` or `[imagePrompt, overlayText, durationMs]`, or objects like `{ image_prompt, image_prompt_attachments, caption, duration_ms }`
14
- - `image_prompt_attachments`: optional public media URLs used as visual reference attachments during image generation
15
- - `meta_details_prompt`: optional guidance for generated title, description, pinned comment, location, and song metadata
16
-
17
- Behavior:
18
-
19
- - `create_slideshow` generates exact 9:16 images, preserves the exact overlay text, and outputs finished slide frames plus a manifest
20
- - `render_video` turns explicit finished slide frames into an auto-advancing vertical MP4
21
- - when chaining operations, pass `result.renderVideoInput` from `create_slideshow` into `render_video` without re-encoding or HTML-escaping the slide URLs
22
- - reserves space for TikTok UI chrome at the top, right edge, and bottom caption area
23
- - keeps subjects away from edge crops and avoids cutting off faces, pets, hands, and products
24
- - prefers centered native-caption placement using image analysis first, with local fallback scoring
25
- - never uses a full paragraph wash behind captions; when enabled, text backgrounds render as tight chips around the words
26
- - restricts caption fonts and text background colors to the checked-in starter option lists in `src/style-options.ts`
27
-
28
- AI dependency contract for agents:
29
-
30
- - the current Vidfarm template standard does not have a first-class `dependencies` field, so this section is the authoritative dependency declaration for this template
31
- - `defaultProvider`, `textModel`, and `imageModel` are config values, not a validated provider-model matrix
32
- - agents should keep provider and model choices inside the combinations listed below unless they are intentionally extending runtime support
33
-
34
- Documented provider-model combinations for this template:
35
-
36
- - `openai`
37
- models:
38
- `image`: `gpt-image-1`, `gpt-image-2`
39
- `text` and `layout_analysis`: `gpt-5.4`
40
- planned but not wired here: `sora-2`
41
- - `gemini`
42
- models:
43
- `image`: `gemini-3.1-flash-image-preview`, `gemini-2.5-flash-image`
44
- `text` and `layout_analysis`: `gemini-3.1-flash-lite`, `gemini-2.5-flash-lite`
45
- planned but not wired here: `veo-3.0-generate-001`
46
- - `openrouter`
47
- models:
48
- `text` and `layout_analysis`: `qwen/qwen3.6-flash`
49
- aspirational image models: `bytedance/seedance-2.0`, `bytedance-seed/seedream-4.5`
50
-
51
- Runtime caveats:
52
-
53
- - `openrouter` text and layout-analysis calls work through the OpenRouter chat-completions path
54
- - `openrouter` image generation is not a first-class adapter path in the current runtime, so do not assume `seedance-2.0` or `seedream-4.5` will work end to end without provider-runtime changes
55
- - OpenAI image generation in the current runtime does not support `image_prompt_attachments`; Gemini is the safest path when reference-image attachments matter
56
- - OpenAI image generation does not expose an exact native `9:16` output size in the current API. For `template_0000`, use portrait output plus explicit `9:16` framing instructions, then preserve composition with contain-plus-blur normalization instead of crop-zooming.
57
- - this template does not expose a video-generation operation, so `sora-2` and `veo-3.0-generate-001` are documented here for future dependency planning only
58
-
59
- Allowed caption fonts:
60
-
61
- - `source_code_pro`
62
- - `montserrat`
63
- - `yesteryear`
64
- - `dm_serif_display`
65
- - `abel`
66
-
67
- Allowed text background colors:
68
-
69
- - `none`
70
- - `black`
71
- - `red`
72
- - `orange`
73
- - `yellow`
74
- - `lime_green`
75
- - `teal`
76
- - `light_blue`
77
- - `dark_blue`
78
- - `violet`
79
- - `pink`
80
- - `brown`
81
- - `dark_green`
82
- - `blue_gray`
83
- - `light_gray`
84
- - `dark_gray`
85
-
86
- Typical routes for this template:
87
-
88
- - `GET /templates/template_0000`
89
- - `GET /templates/template_0000/skill`
90
- - `POST /templates/template_0000/config`
91
- - `POST /templates/template_0000/operations/create_slideshow`
92
- - `POST /templates/template_0000/operations/render_video`
93
- - `GET /templates/template_0000/jobs`
94
- - `GET /templates/template_0000/jobs/:jobId`
95
- - `GET /templates/template_0000/jobs/:jobId/logs`
96
- - `POST /templates/template_0000/jobs/:jobId/cancel`
97
-
98
- Shared shell variables:
99
-
100
- ```bash
101
- BASE_URL="http://localhost:3000"
102
- VIDFARM_USER_ID="replace-me"
103
- VIDFARM_API_KEY="replace-me"
104
- COMMON_HEADERS=(
105
- -H "vidfarm-user-id: ${VIDFARM_USER_ID}"
106
- -H "vidfarm-api-key: ${VIDFARM_API_KEY}"
107
- )
108
- JSON_HEADERS=(
109
- "${COMMON_HEADERS[@]}"
110
- -H "content-type: application/json"
111
- )
112
- ```
113
-
114
- Get template metadata:
115
-
116
- ```bash
117
- curl "${BASE_URL}/templates/template_0000" \
118
- "${COMMON_HEADERS[@]}"
119
- ```
120
-
121
- Get the published `SKILL.md` URL contents:
122
-
123
- ```bash
124
- curl "${BASE_URL}/templates/template_0000/skill" \
125
- "${COMMON_HEADERS[@]}"
126
- ```
127
-
128
- Save template config:
129
-
130
- ```bash
131
- curl -X POST "${BASE_URL}/templates/template_0000/config" \
132
- "${JSON_HEADERS[@]}" \
133
- -d '{
134
- "config": {
135
- "defaultProvider": "openrouter",
136
- "textModel": "openai/gpt-4.1-mini",
137
- "imageModel": "bytedance-seed/seedream-4.5",
138
- "captionFont": "montserrat",
139
- "captionBackgroundColor": "none",
140
- "renderCompositionId": "template-0000"
141
- }
142
- }'
143
- ```
144
-
145
- Create a slideshow:
146
-
147
- ```bash
148
- curl -X POST "${BASE_URL}/templates/template_0000/operations/create_slideshow" \
149
- "${JSON_HEADERS[@]}" \
150
- -d '{
151
- "tracer": "launch-story-001",
152
- "payload": {
153
- "slides": [
154
- ["late night founder at laptop", "launching after hours", 2200],
155
- {
156
- "image_prompt": "skincare serum bottle on a clean bathroom counter near a window",
157
- "caption": "the routine was simpler than i thought",
158
- "duration_ms": 2800
159
- }
160
- ],
161
- "meta_details_prompt": "Write native TikTok metadata for a US audience. Keep it curiosity-driven and casual."
162
- }
163
- }'
164
- ```
165
-
166
- Render the final video:
167
-
168
- ```bash
169
- curl -X POST "${BASE_URL}/templates/template_0000/operations/render_video" \
170
- "${JSON_HEADERS[@]}" \
171
- -d '{
172
- "tracer": "launch-story-001-render",
173
- "payload": {
174
- "slides": [
175
- {
176
- "imageUrl": "https://example.com/slide-1.png",
177
- "durationMs": 2200
178
- },
179
- {
180
- "imageUrl": "https://example.com/slide-2.png",
181
- "durationMs": 2800
182
- }
183
- ]
184
- }
185
- }'
186
- ```
187
-
188
- List jobs for this template:
189
-
190
- ```bash
191
- curl "${BASE_URL}/templates/template_0000/jobs" \
192
- "${COMMON_HEADERS[@]}"
193
- ```
194
-
195
- List jobs filtered by tracer and date range:
196
-
197
- ```bash
198
- curl "${BASE_URL}/templates/template_0000/jobs?tracer=launch-story-001&start_time=2026-05-17T00:00:00.000Z&end_time=2026-05-18T00:00:00.000Z&limit=20" \
199
- "${COMMON_HEADERS[@]}"
200
- ```
201
-
202
- Get one job:
203
-
204
- ```bash
205
- JOB_ID="job_replace_me"
206
-
207
- curl "${BASE_URL}/templates/template_0000/jobs/${JOB_ID}" \
208
- "${COMMON_HEADERS[@]}"
209
- ```
210
-
211
- Get one job's logs:
212
-
213
- ```bash
214
- curl "${BASE_URL}/templates/template_0000/jobs/${JOB_ID}/logs?start_time=2026-05-17T00:00:00.000Z&end_time=2026-05-18T00:00:00.000Z&limit=200" \
215
- "${COMMON_HEADERS[@]}"
216
- ```
217
-
218
- Cancel one job:
219
-
220
- ```bash
221
- curl -X POST "${BASE_URL}/templates/template_0000/jobs/${JOB_ID}/cancel" \
222
- "${COMMON_HEADERS[@]}"
223
- ```
224
-
225
- Use this template for short-form product explainers, founder clips, launch teasers, and simple storyboarding workflows where speed matters more than deep custom motion design.
@@ -1,11 +0,0 @@
1
- {
2
- "slides": [
3
- {
4
- "imageUrl": "https://images.unsplash.com/photo-1519389950473-47ba0277781c?auto=format&fit=crop&w=1080&q=80"
5
- },
6
- {
7
- "imageUrl": "https://images.unsplash.com/photo-1520607162513-77705c0f0d4a?auto=format&fit=crop&w=1080&q=80"
8
- }
9
- ],
10
- "secondsPerSlide": 2
11
- }
@@ -1,27 +0,0 @@
1
- {
2
- "name": "vidfarm_template_0000",
3
- "version": "1.0.0",
4
- "private": true,
5
- "description": "Standalone Vidfarm template_0000 starter project for local template authoring.",
6
- "type": "module",
7
- "scripts": {
8
- "check": "tsc -p tsconfig.json --noEmit",
9
- "build": "tsc -p tsconfig.json",
10
- "studio": "remotion studio src/remotion/index.tsx"
11
- },
12
- "dependencies": {
13
- "@remotion/bundler": "4.0.355",
14
- "@remotion/cli": "4.0.355",
15
- "@remotion/renderer": "4.0.355",
16
- "react": "^18.3.1",
17
- "react-dom": "^18.3.1",
18
- "remotion": "4.0.355",
19
- "sharp": "^0.34.2"
20
- },
21
- "devDependencies": {
22
- "@types/node": "^24.0.1",
23
- "@types/react": "^18.3.23",
24
- "@types/react-dom": "^18.3.7",
25
- "typescript": "^5.8.3"
26
- }
27
- }
@@ -1,7 +0,0 @@
1
- # Source Notes
2
-
3
- - original format URL:
4
- - creator/account:
5
- - why it wins:
6
- - what must survive adaptation:
7
- - what can change for new brands:
@@ -1,202 +0,0 @@
1
- import sharp from "sharp";
2
- export async function normalizeToPortraitFrame(input, target = { width: 1080, height: 1920 }) {
3
- const buffer = Buffer.isBuffer(input) ? input : Buffer.from(input);
4
- const oriented = sharp(buffer, { density: 144 }).rotate();
5
- const trimmed = await trimFlatBorders(oriented);
6
- const targetAspect = target.width / target.height;
7
- const shouldPreserveFraming = await isCloseToAspect(trimmed, targetAspect, 0.025);
8
- const cropped = shouldPreserveFraming ? trimmed : await cropToActiveImageRegion(trimmed);
9
- const normalized = await resizeWithBlurredContain(cropped, target, targetAspect);
10
- return {
11
- bytes: normalized,
12
- contentType: "image/png",
13
- width: target.width,
14
- height: target.height
15
- };
16
- }
17
- async function resizeWithBlurredContain(image, target, targetAspect) {
18
- const metadata = await image.metadata();
19
- const width = metadata.width ?? 0;
20
- const height = metadata.height ?? 0;
21
- if (!width || !height) {
22
- return image
23
- .resize(target.width, target.height, {
24
- fit: "cover",
25
- position: sharp.strategy.attention
26
- })
27
- .png()
28
- .toBuffer();
29
- }
30
- const aspect = width / height;
31
- if (Math.abs(aspect - targetAspect) <= 0.015) {
32
- return image
33
- .resize(target.width, target.height, {
34
- fit: "fill"
35
- })
36
- .png()
37
- .toBuffer();
38
- }
39
- const foreground = await image
40
- .clone()
41
- .resize(target.width, target.height, {
42
- fit: "contain",
43
- background: { r: 0, g: 0, b: 0, alpha: 0 }
44
- })
45
- .png()
46
- .toBuffer();
47
- const background = await image
48
- .clone()
49
- .resize(target.width, target.height, {
50
- fit: "cover",
51
- position: sharp.strategy.attention
52
- })
53
- .blur(20)
54
- .modulate({ brightness: 0.92, saturation: 0.9 })
55
- .png()
56
- .toBuffer();
57
- return sharp(background)
58
- .composite([{ input: foreground }])
59
- .png()
60
- .toBuffer();
61
- }
62
- async function isCloseToAspect(image, targetAspect, tolerance) {
63
- const metadata = await image.metadata();
64
- const width = metadata.width ?? 0;
65
- const height = metadata.height ?? 0;
66
- if (!width || !height) {
67
- return false;
68
- }
69
- return Math.abs(width / height - targetAspect) <= tolerance;
70
- }
71
- async function trimFlatBorders(image) {
72
- const metadata = await image.metadata();
73
- const sourceWidth = metadata.width ?? 0;
74
- const sourceHeight = metadata.height ?? 0;
75
- if (!sourceWidth || !sourceHeight || sourceWidth < 3 || sourceHeight < 3) {
76
- return image;
77
- }
78
- const { data, info } = await image
79
- .clone()
80
- .trim({ threshold: 16 })
81
- .png()
82
- .toBuffer({ resolveWithObject: true });
83
- const widthRatio = info.width / sourceWidth;
84
- const heightRatio = info.height / sourceHeight;
85
- if (widthRatio > 0.985 && heightRatio > 0.985) {
86
- return image;
87
- }
88
- return sharp(data).rotate();
89
- }
90
- async function cropToActiveImageRegion(image) {
91
- const metadata = await image.metadata();
92
- const sourceWidth = metadata.width ?? 0;
93
- const sourceHeight = metadata.height ?? 0;
94
- if (!sourceWidth || !sourceHeight) {
95
- return image;
96
- }
97
- const sampleWidth = 96;
98
- const sampleHeight = 170;
99
- const sample = await image
100
- .clone()
101
- .resize(sampleWidth, sampleHeight, { fit: "fill" })
102
- .grayscale()
103
- .raw()
104
- .toBuffer();
105
- const bounds = detectActiveBounds(sample, sampleWidth, sampleHeight);
106
- if (!bounds) {
107
- return image;
108
- }
109
- const left = Math.max(0, Math.floor((bounds.left / sampleWidth) * sourceWidth));
110
- const top = Math.max(0, Math.floor((bounds.top / sampleHeight) * sourceHeight));
111
- const width = Math.min(sourceWidth - left, Math.max(1, Math.ceil((bounds.width / sampleWidth) * sourceWidth)));
112
- const height = Math.min(sourceHeight - top, Math.max(1, Math.ceil((bounds.height / sampleHeight) * sourceHeight)));
113
- return image.extract({ left, top, width, height });
114
- }
115
- function detectActiveBounds(sample, width, height) {
116
- const rowActivity = new Array(height).fill(0);
117
- const colActivity = new Array(width).fill(0);
118
- for (let y = 0; y < height; y += 1) {
119
- for (let x = 0; x < width; x += 1) {
120
- const index = y * width + x;
121
- const current = sample[index] ?? 0;
122
- const left = x > 0 ? sample[index - 1] ?? current : current;
123
- const up = y > 0 ? sample[index - width] ?? current : current;
124
- const right = x < width - 1 ? sample[index + 1] ?? current : current;
125
- const down = y < height - 1 ? sample[index + width] ?? current : current;
126
- const energy = Math.abs(current - left) +
127
- Math.abs(current - right) +
128
- Math.abs(current - up) +
129
- Math.abs(current - down);
130
- rowActivity[y] += energy;
131
- colActivity[x] += energy;
132
- }
133
- }
134
- const smoothedRows = smoothSeries(rowActivity.map((value) => value / width), 9);
135
- const smoothedCols = smoothSeries(colActivity.map((value) => value / height), 7);
136
- const rowBounds = findActiveRange(smoothedRows, Math.round(height * 0.18));
137
- const colBounds = findActiveRange(smoothedCols, Math.round(width * 0.16));
138
- if (!rowBounds || !colBounds) {
139
- return null;
140
- }
141
- const topMargin = rowBounds.start / height;
142
- const bottomMargin = (height - rowBounds.end - 1) / height;
143
- const leftMargin = colBounds.start / width;
144
- const rightMargin = (width - colBounds.end - 1) / width;
145
- const croppedWidth = colBounds.end - colBounds.start + 1;
146
- const croppedHeight = rowBounds.end - rowBounds.start + 1;
147
- const croppedAreaRatio = (croppedWidth * croppedHeight) / (width * height);
148
- const significantMargins = topMargin > 0.11 || bottomMargin > 0.11 || leftMargin > 0.08 || rightMargin > 0.08;
149
- if (!significantMargins && croppedAreaRatio > 0.9) {
150
- return null;
151
- }
152
- return {
153
- left: colBounds.start,
154
- top: rowBounds.start,
155
- width: croppedWidth,
156
- height: croppedHeight
157
- };
158
- }
159
- function findActiveRange(values, minSpan) {
160
- const mean = values.reduce((sum, value) => sum + value, 0) / Math.max(values.length, 1);
161
- const max = values.reduce((best, value) => Math.max(best, value), 0);
162
- if (max <= 0) {
163
- return null;
164
- }
165
- const threshold = mean + (max - mean) * 0.18;
166
- let start = 0;
167
- while (start < values.length && values[start] < threshold) {
168
- start += 1;
169
- }
170
- let end = values.length - 1;
171
- while (end >= 0 && values[end] < threshold) {
172
- end -= 1;
173
- }
174
- if (start >= end) {
175
- return null;
176
- }
177
- const desiredSpan = Math.max(minSpan, end - start + 1);
178
- const extra = desiredSpan - (end - start + 1);
179
- const expandStart = Math.floor(extra / 2);
180
- const expandEnd = extra - expandStart;
181
- start = Math.max(0, start - expandStart - 4);
182
- end = Math.min(values.length - 1, end + expandEnd + 4);
183
- return { start, end };
184
- }
185
- function smoothSeries(values, radius) {
186
- return values.map((_, index) => {
187
- let total = 0;
188
- let count = 0;
189
- for (let offset = -radius; offset <= radius; offset += 1) {
190
- const value = values[index + offset];
191
- if (value === undefined) {
192
- continue;
193
- }
194
- total += value;
195
- count += 1;
196
- }
197
- return total / Math.max(count, 1);
198
- });
199
- }
200
- function clamp(value, min, max) {
201
- return Math.min(Math.max(value, min), max);
202
- }