@studiomeyer/mcp-video 1.0.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 (184) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
  3. package/.github/workflows/ci.yml +34 -0
  4. package/CHANGELOG.md +24 -0
  5. package/CONTRIBUTING.md +75 -0
  6. package/LICENSE +21 -0
  7. package/README.md +198 -0
  8. package/USAGE.md +144 -0
  9. package/dist/handlers/capcut.d.ts +6 -0
  10. package/dist/handlers/capcut.js +229 -0
  11. package/dist/handlers/capcut.js.map +1 -0
  12. package/dist/handlers/editing.d.ts +6 -0
  13. package/dist/handlers/editing.js +242 -0
  14. package/dist/handlers/editing.js.map +1 -0
  15. package/dist/handlers/index.d.ts +2 -0
  16. package/dist/handlers/index.js +33 -0
  17. package/dist/handlers/index.js.map +1 -0
  18. package/dist/handlers/post-production.d.ts +5 -0
  19. package/dist/handlers/post-production.js +109 -0
  20. package/dist/handlers/post-production.js.map +1 -0
  21. package/dist/handlers/smart-screenshot.d.ts +5 -0
  22. package/dist/handlers/smart-screenshot.js +83 -0
  23. package/dist/handlers/smart-screenshot.js.map +1 -0
  24. package/dist/handlers/tts.d.ts +5 -0
  25. package/dist/handlers/tts.js +83 -0
  26. package/dist/handlers/tts.js.map +1 -0
  27. package/dist/handlers/video.d.ts +5 -0
  28. package/dist/handlers/video.js +127 -0
  29. package/dist/handlers/video.js.map +1 -0
  30. package/dist/lib/dual-transport.d.ts +42 -0
  31. package/dist/lib/dual-transport.js +208 -0
  32. package/dist/lib/dual-transport.js.map +1 -0
  33. package/dist/lib/logger.d.ts +12 -0
  34. package/dist/lib/logger.js +42 -0
  35. package/dist/lib/logger.js.map +1 -0
  36. package/dist/lib/types.d.ts +16 -0
  37. package/dist/lib/types.js +15 -0
  38. package/dist/lib/types.js.map +1 -0
  39. package/dist/schemas/capcut.d.ts +608 -0
  40. package/dist/schemas/capcut.js +411 -0
  41. package/dist/schemas/capcut.js.map +1 -0
  42. package/dist/schemas/editing.d.ts +822 -0
  43. package/dist/schemas/editing.js +466 -0
  44. package/dist/schemas/editing.js.map +1 -0
  45. package/dist/schemas/index.d.ts +2366 -0
  46. package/dist/schemas/index.js +15 -0
  47. package/dist/schemas/index.js.map +1 -0
  48. package/dist/schemas/post-production.d.ts +379 -0
  49. package/dist/schemas/post-production.js +268 -0
  50. package/dist/schemas/post-production.js.map +1 -0
  51. package/dist/schemas/smart-screenshot.d.ts +127 -0
  52. package/dist/schemas/smart-screenshot.js +122 -0
  53. package/dist/schemas/smart-screenshot.js.map +1 -0
  54. package/dist/schemas/tts.d.ts +220 -0
  55. package/dist/schemas/tts.js +194 -0
  56. package/dist/schemas/tts.js.map +1 -0
  57. package/dist/schemas/video.d.ts +236 -0
  58. package/dist/schemas/video.js +210 -0
  59. package/dist/schemas/video.js.map +1 -0
  60. package/dist/server.d.ts +11 -0
  61. package/dist/server.js +239 -0
  62. package/dist/server.js.map +1 -0
  63. package/dist/server.test.d.ts +1 -0
  64. package/dist/server.test.js +87 -0
  65. package/dist/server.test.js.map +1 -0
  66. package/dist/tools/engine/audio-mixer.d.ts +40 -0
  67. package/dist/tools/engine/audio-mixer.js +169 -0
  68. package/dist/tools/engine/audio-mixer.js.map +1 -0
  69. package/dist/tools/engine/audio.d.ts +22 -0
  70. package/dist/tools/engine/audio.js +73 -0
  71. package/dist/tools/engine/audio.js.map +1 -0
  72. package/dist/tools/engine/beat-sync.d.ts +31 -0
  73. package/dist/tools/engine/beat-sync.js +270 -0
  74. package/dist/tools/engine/beat-sync.js.map +1 -0
  75. package/dist/tools/engine/capture.d.ts +12 -0
  76. package/dist/tools/engine/capture.js +290 -0
  77. package/dist/tools/engine/capture.js.map +1 -0
  78. package/dist/tools/engine/chroma-key.d.ts +27 -0
  79. package/dist/tools/engine/chroma-key.js +154 -0
  80. package/dist/tools/engine/chroma-key.js.map +1 -0
  81. package/dist/tools/engine/concat.d.ts +49 -0
  82. package/dist/tools/engine/concat.js +149 -0
  83. package/dist/tools/engine/concat.js.map +1 -0
  84. package/dist/tools/engine/cursor.d.ts +26 -0
  85. package/dist/tools/engine/cursor.js +185 -0
  86. package/dist/tools/engine/cursor.js.map +1 -0
  87. package/dist/tools/engine/easing.d.ts +15 -0
  88. package/dist/tools/engine/easing.js +100 -0
  89. package/dist/tools/engine/easing.js.map +1 -0
  90. package/dist/tools/engine/editing.d.ts +158 -0
  91. package/dist/tools/engine/editing.js +541 -0
  92. package/dist/tools/engine/editing.js.map +1 -0
  93. package/dist/tools/engine/encoder.d.ts +31 -0
  94. package/dist/tools/engine/encoder.js +154 -0
  95. package/dist/tools/engine/encoder.js.map +1 -0
  96. package/dist/tools/engine/index.d.ts +30 -0
  97. package/dist/tools/engine/index.js +23 -0
  98. package/dist/tools/engine/index.js.map +1 -0
  99. package/dist/tools/engine/lut-presets.d.ts +25 -0
  100. package/dist/tools/engine/lut-presets.js +141 -0
  101. package/dist/tools/engine/lut-presets.js.map +1 -0
  102. package/dist/tools/engine/narrated-video.d.ts +63 -0
  103. package/dist/tools/engine/narrated-video.js +163 -0
  104. package/dist/tools/engine/narrated-video.js.map +1 -0
  105. package/dist/tools/engine/scenes.d.ts +17 -0
  106. package/dist/tools/engine/scenes.js +223 -0
  107. package/dist/tools/engine/scenes.js.map +1 -0
  108. package/dist/tools/engine/smart-screenshot.d.ts +80 -0
  109. package/dist/tools/engine/smart-screenshot.js +744 -0
  110. package/dist/tools/engine/smart-screenshot.js.map +1 -0
  111. package/dist/tools/engine/social-format.d.ts +66 -0
  112. package/dist/tools/engine/social-format.js +107 -0
  113. package/dist/tools/engine/social-format.js.map +1 -0
  114. package/dist/tools/engine/template-renderer.d.ts +45 -0
  115. package/dist/tools/engine/template-renderer.js +233 -0
  116. package/dist/tools/engine/template-renderer.js.map +1 -0
  117. package/dist/tools/engine/templates.d.ts +87 -0
  118. package/dist/tools/engine/templates.js +272 -0
  119. package/dist/tools/engine/templates.js.map +1 -0
  120. package/dist/tools/engine/text-animations.d.ts +33 -0
  121. package/dist/tools/engine/text-animations.js +192 -0
  122. package/dist/tools/engine/text-animations.js.map +1 -0
  123. package/dist/tools/engine/text-overlay.d.ts +27 -0
  124. package/dist/tools/engine/text-overlay.js +84 -0
  125. package/dist/tools/engine/text-overlay.js.map +1 -0
  126. package/dist/tools/engine/tts.d.ts +54 -0
  127. package/dist/tools/engine/tts.js +186 -0
  128. package/dist/tools/engine/tts.js.map +1 -0
  129. package/dist/tools/engine/types.d.ts +166 -0
  130. package/dist/tools/engine/types.js +13 -0
  131. package/dist/tools/engine/types.js.map +1 -0
  132. package/dist/tools/engine/voice-effects.d.ts +18 -0
  133. package/dist/tools/engine/voice-effects.js +215 -0
  134. package/dist/tools/engine/voice-effects.js.map +1 -0
  135. package/dist/tools/index.d.ts +32 -0
  136. package/dist/tools/index.js +23 -0
  137. package/dist/tools/index.js.map +1 -0
  138. package/package.json +56 -0
  139. package/scripts/check-deps.js +39 -0
  140. package/src/handlers/capcut.ts +245 -0
  141. package/src/handlers/editing.ts +260 -0
  142. package/src/handlers/index.ts +34 -0
  143. package/src/handlers/post-production.ts +136 -0
  144. package/src/handlers/smart-screenshot.ts +86 -0
  145. package/src/handlers/tts.ts +103 -0
  146. package/src/handlers/video.ts +137 -0
  147. package/src/lib/dual-transport.ts +272 -0
  148. package/src/lib/logger.ts +59 -0
  149. package/src/lib/types.ts +25 -0
  150. package/src/schemas/capcut.ts +418 -0
  151. package/src/schemas/editing.ts +476 -0
  152. package/src/schemas/index.ts +15 -0
  153. package/src/schemas/post-production.ts +273 -0
  154. package/src/schemas/smart-screenshot.ts +122 -0
  155. package/src/schemas/tts.ts +197 -0
  156. package/src/schemas/video.ts +211 -0
  157. package/src/server.test.ts +99 -0
  158. package/src/server.ts +289 -0
  159. package/src/tools/engine/audio-mixer.ts +244 -0
  160. package/src/tools/engine/audio.ts +115 -0
  161. package/src/tools/engine/beat-sync.ts +356 -0
  162. package/src/tools/engine/capture.ts +360 -0
  163. package/src/tools/engine/chroma-key.ts +202 -0
  164. package/src/tools/engine/concat.ts +242 -0
  165. package/src/tools/engine/cursor.ts +222 -0
  166. package/src/tools/engine/easing.ts +120 -0
  167. package/src/tools/engine/editing.ts +809 -0
  168. package/src/tools/engine/encoder.ts +208 -0
  169. package/src/tools/engine/index.ts +33 -0
  170. package/src/tools/engine/lut-presets.ts +235 -0
  171. package/src/tools/engine/narrated-video.ts +267 -0
  172. package/src/tools/engine/scenes.ts +309 -0
  173. package/src/tools/engine/smart-screenshot.ts +923 -0
  174. package/src/tools/engine/social-format.ts +146 -0
  175. package/src/tools/engine/template-renderer.ts +294 -0
  176. package/src/tools/engine/templates.ts +370 -0
  177. package/src/tools/engine/text-animations.ts +282 -0
  178. package/src/tools/engine/text-overlay.ts +143 -0
  179. package/src/tools/engine/tts.ts +284 -0
  180. package/src/tools/engine/types.ts +191 -0
  181. package/src/tools/engine/voice-effects.ts +258 -0
  182. package/src/tools/index.ts +67 -0
  183. package/tsconfig.json +19 -0
  184. package/vitest.config.ts +7 -0
@@ -0,0 +1,744 @@
1
+ /**
2
+ * Smart Screenshot Engine
3
+ *
4
+ * Element-aware screenshot system that can target specific page features.
5
+ * Instead of full-page screenshots, this finds and captures specific UI elements
6
+ * like chat widgets, booking forms, pricing sections, wizards, etc.
7
+ *
8
+ * Usage:
9
+ * smartScreenshot({ url: '...', targets: ['chat', 'pricing', 'booking'] })
10
+ * smartScreenshot({ url: '...', targets: [{ selector: '.hero-section' }] })
11
+ * smartScreenshot({ url: '...', targets: ['all'] }) // auto-detect all features
12
+ */
13
+ import { chromium } from 'playwright';
14
+ import * as fs from 'fs';
15
+ import * as path from 'path';
16
+ import { logger } from '../../lib/logger.js';
17
+ const OUTPUT_DIR = process.env.VIDEO_OUTPUT_DIR || './output';
18
+ const FEATURE_PATTERNS = [
19
+ {
20
+ name: 'Hero Section',
21
+ keywords: ['hero', 'header', 'banner', 'above-fold', 'startseite', 'landing'],
22
+ selectors: [
23
+ '[class*="hero"]', '[id*="hero"]',
24
+ '[class*="Hero"]', '[id*="Hero"]',
25
+ 'section:first-of-type',
26
+ 'main > section:first-child',
27
+ 'main > div:first-child',
28
+ '[class*="banner"]:not([class*="cookie"])',
29
+ '[class*="landing"]',
30
+ '[class*="jumbotron"]',
31
+ ],
32
+ textPatterns: [],
33
+ ariaRoles: ['banner'],
34
+ minSize: { width: 600, height: 300 },
35
+ padding: 0,
36
+ },
37
+ {
38
+ name: 'Chat Widget',
39
+ keywords: ['chat', 'chatbot', 'messenger', 'live-chat', 'support-chat', 'bot'],
40
+ selectors: [
41
+ '[class*="chat"]', '[id*="chat"]',
42
+ '[class*="Chat"]', '[id*="Chat"]',
43
+ '[class*="chatbot"]', '[id*="chatbot"]',
44
+ '[class*="messenger"]',
45
+ '[class*="widget"]',
46
+ '[class*="intercom"]',
47
+ '[class*="crisp"]',
48
+ '[class*="tawk"]',
49
+ '[class*="zendesk"]',
50
+ '[data-testid*="chat"]',
51
+ 'iframe[src*="chat"]',
52
+ ],
53
+ textPatterns: [/chat/i, /nachricht/i, /fragen/i, /hilfe/i],
54
+ ariaRoles: ['dialog', 'complementary'],
55
+ minSize: { width: 200, height: 200 },
56
+ padding: 10,
57
+ scrollTo: false,
58
+ waitFor: true,
59
+ revealSelector: '[class*="chat"] button, [class*="Chat"] button, [class*="chat-trigger"], [class*="chat-toggle"], button[aria-label*="chat" i], button[aria-label*="Chat"]',
60
+ },
61
+ {
62
+ name: 'Booking System',
63
+ keywords: ['booking', 'buchung', 'reservation', 'reservierung', 'buchen', 'termin', 'appointment', 'kalender', 'calendar'],
64
+ selectors: [
65
+ '[class*="booking"]', '[id*="booking"]',
66
+ '[class*="Booking"]', '[id*="Booking"]',
67
+ '[class*="reservation"]', '[id*="reservation"]',
68
+ '[class*="calendar"]', '[id*="calendar"]',
69
+ '[class*="appointment"]',
70
+ '[class*="scheduler"]',
71
+ '[class*="datepicker"]',
72
+ 'form[action*="book"]',
73
+ 'form[action*="reserv"]',
74
+ ],
75
+ textPatterns: [/buchen/i, /reserv/i, /termin/i, /book/i, /appointment/i],
76
+ ariaRoles: ['form'],
77
+ minSize: { width: 300, height: 200 },
78
+ padding: 20,
79
+ scrollTo: true,
80
+ },
81
+ {
82
+ name: 'Pricing Section',
83
+ keywords: ['pricing', 'preise', 'preis', 'tarife', 'pakete', 'plans', 'packages', 'kosten', 'abo'],
84
+ selectors: [
85
+ '[class*="pricing"]', '[id*="pricing"]',
86
+ '[class*="Pricing"]', '[id*="Pricing"]',
87
+ '[class*="price"]', '[id*="price"]',
88
+ '[class*="plans"]', '[id*="plans"]',
89
+ '[class*="packages"]', '[id*="packages"]',
90
+ '[class*="tarif"]', '[id*="tarif"]',
91
+ ],
92
+ textPatterns: [/\d+\s*[€$£]/i, /pro monat/i, /per month/i, /\/mo/i, /pricing/i, /preise/i],
93
+ ariaRoles: [],
94
+ minSize: { width: 400, height: 200 },
95
+ padding: 20,
96
+ scrollTo: true,
97
+ },
98
+ {
99
+ name: 'Contact Form',
100
+ keywords: ['contact', 'kontakt', 'form', 'formular', 'anfrage', 'inquiry', 'nachricht', 'message'],
101
+ selectors: [
102
+ '[class*="contact"]', '[id*="contact"]',
103
+ '[class*="Contact"]', '[id*="Contact"]',
104
+ '[class*="kontakt"]', '[id*="kontakt"]',
105
+ 'form:not([class*="search"]):not([class*="login"]):not([class*="newsletter"])',
106
+ '[class*="inquiry"]',
107
+ '[class*="anfrage"]',
108
+ ],
109
+ textPatterns: [/kontakt/i, /contact/i, /nachricht senden/i, /send message/i, /anfrage/i],
110
+ ariaRoles: ['form'],
111
+ minSize: { width: 300, height: 200 },
112
+ padding: 20,
113
+ scrollTo: true,
114
+ },
115
+ {
116
+ name: 'Navigation',
117
+ keywords: ['nav', 'navigation', 'menu', 'header-nav', 'navbar', 'menubar'],
118
+ selectors: [
119
+ 'nav', 'header nav',
120
+ '[class*="nav"]', '[id*="nav"]',
121
+ '[class*="Nav"]', '[id*="Nav"]',
122
+ '[class*="navbar"]',
123
+ '[class*="menu"]:not([class*="footer"])',
124
+ '[role="navigation"]',
125
+ ],
126
+ textPatterns: [],
127
+ ariaRoles: ['navigation', 'menubar'],
128
+ minSize: { width: 600, height: 40 },
129
+ padding: 5,
130
+ scrollTo: false,
131
+ },
132
+ {
133
+ name: 'Footer',
134
+ keywords: ['footer', 'fusszeile'],
135
+ selectors: [
136
+ 'footer', '[class*="footer"]', '[id*="footer"]',
137
+ '[class*="Footer"]', '[id*="Footer"]',
138
+ '[role="contentinfo"]',
139
+ ],
140
+ textPatterns: [],
141
+ ariaRoles: ['contentinfo'],
142
+ minSize: { width: 600, height: 100 },
143
+ padding: 0,
144
+ scrollTo: true,
145
+ },
146
+ {
147
+ name: 'Gallery / Portfolio',
148
+ keywords: ['gallery', 'galerie', 'portfolio', 'showcase', 'projekte', 'projects', 'work', 'arbeiten'],
149
+ selectors: [
150
+ '[class*="gallery"]', '[id*="gallery"]',
151
+ '[class*="Gallery"]', '[id*="Gallery"]',
152
+ '[class*="portfolio"]', '[id*="portfolio"]',
153
+ '[class*="Portfolio"]', '[id*="Portfolio"]',
154
+ '[class*="showcase"]',
155
+ '[class*="projects"]', '[id*="projects"]',
156
+ '[class*="grid"]:has(img)',
157
+ ],
158
+ textPatterns: [/portfolio/i, /projekte/i, /projects/i, /galerie/i, /gallery/i, /unsere arbeit/i],
159
+ ariaRoles: [],
160
+ minSize: { width: 400, height: 300 },
161
+ padding: 20,
162
+ scrollTo: true,
163
+ },
164
+ {
165
+ name: 'Wizard / Stepper',
166
+ keywords: ['wizard', 'stepper', 'steps', 'schritte', 'onboarding', 'flow', 'multi-step', 'progress'],
167
+ selectors: [
168
+ '[class*="wizard"]', '[id*="wizard"]',
169
+ '[class*="Wizard"]', '[id*="Wizard"]',
170
+ '[class*="stepper"]', '[id*="stepper"]',
171
+ '[class*="steps"]', '[id*="steps"]',
172
+ '[class*="onboarding"]', '[id*="onboarding"]',
173
+ '[class*="progress-bar"]',
174
+ '[class*="step-indicator"]',
175
+ '[role="progressbar"]',
176
+ ],
177
+ textPatterns: [/schritt \d/i, /step \d/i],
178
+ ariaRoles: ['progressbar'],
179
+ minSize: { width: 300, height: 200 },
180
+ padding: 20,
181
+ scrollTo: true,
182
+ },
183
+ {
184
+ name: 'Testimonials / Reviews',
185
+ keywords: ['testimonial', 'review', 'bewertung', 'kundenstimmen', 'feedback', 'referenzen'],
186
+ selectors: [
187
+ '[class*="testimonial"]', '[id*="testimonial"]',
188
+ '[class*="Testimonial"]', '[id*="Testimonial"]',
189
+ '[class*="review"]', '[id*="review"]',
190
+ '[class*="feedback"]',
191
+ '[class*="quote"]',
192
+ ],
193
+ textPatterns: [/testimonial/i, /bewertung/i, /kundenstimm/i, /review/i],
194
+ ariaRoles: [],
195
+ minSize: { width: 300, height: 150 },
196
+ padding: 20,
197
+ scrollTo: true,
198
+ },
199
+ {
200
+ name: 'Services / Features',
201
+ keywords: ['services', 'leistungen', 'features', 'funktionen', 'angebot', 'offering'],
202
+ selectors: [
203
+ '[class*="services"]', '[id*="services"]',
204
+ '[class*="Services"]', '[id*="Services"]',
205
+ '[class*="features"]', '[id*="features"]',
206
+ '[class*="Features"]', '[id*="Features"]',
207
+ '[class*="leistung"]', '[id*="leistung"]',
208
+ '[class*="offering"]',
209
+ ],
210
+ textPatterns: [/leistungen/i, /services/i, /features/i, /was wir/i, /what we/i],
211
+ ariaRoles: [],
212
+ minSize: { width: 400, height: 200 },
213
+ padding: 20,
214
+ scrollTo: true,
215
+ },
216
+ {
217
+ name: 'CTA / Call-to-Action',
218
+ keywords: ['cta', 'call-to-action', 'button', 'action', 'jetzt-starten', 'get-started'],
219
+ selectors: [
220
+ '[class*="cta"]', '[id*="cta"]',
221
+ '[class*="CTA"]', '[id*="CTA"]',
222
+ '[class*="call-to-action"]',
223
+ 'a[class*="btn-primary"]',
224
+ 'a[class*="button-primary"]',
225
+ '[class*="hero"] a[class*="btn"]',
226
+ '[class*="hero"] button',
227
+ ],
228
+ textPatterns: [/jetzt starten/i, /get started/i, /kostenlos/i, /free trial/i, /jetzt buchen/i],
229
+ ariaRoles: [],
230
+ minSize: { width: 100, height: 30 },
231
+ padding: 30,
232
+ scrollTo: true,
233
+ },
234
+ {
235
+ name: 'Map / Location',
236
+ keywords: ['map', 'karte', 'standort', 'location', 'anfahrt', 'directions'],
237
+ selectors: [
238
+ '[class*="map"]', '[id*="map"]',
239
+ '[class*="Map"]', '[id*="Map"]',
240
+ '[class*="location"]', '[id*="location"]',
241
+ '[class*="standort"]',
242
+ 'iframe[src*="maps"]',
243
+ 'iframe[src*="google.com/maps"]',
244
+ '.mapboxgl-map',
245
+ '.leaflet-container',
246
+ ],
247
+ textPatterns: [/standort/i, /location/i, /anfahrt/i],
248
+ ariaRoles: [],
249
+ minSize: { width: 300, height: 200 },
250
+ padding: 10,
251
+ scrollTo: true,
252
+ },
253
+ {
254
+ name: 'Video Section',
255
+ keywords: ['video', 'player', 'media'],
256
+ selectors: [
257
+ 'video', '[class*="video"]', '[id*="video"]',
258
+ '[class*="Video"]', '[id*="Video"]',
259
+ '[class*="player"]',
260
+ 'iframe[src*="youtube"]',
261
+ 'iframe[src*="vimeo"]',
262
+ ],
263
+ textPatterns: [],
264
+ ariaRoles: [],
265
+ minSize: { width: 300, height: 200 },
266
+ padding: 10,
267
+ scrollTo: true,
268
+ },
269
+ {
270
+ name: 'Newsletter / Subscribe',
271
+ keywords: ['newsletter', 'subscribe', 'abonnieren', 'email-signup'],
272
+ selectors: [
273
+ '[class*="newsletter"]', '[id*="newsletter"]',
274
+ '[class*="Newsletter"]', '[id*="Newsletter"]',
275
+ '[class*="subscribe"]', '[id*="subscribe"]',
276
+ 'form:has(input[type="email"]):not(:has(input[type="password"]))',
277
+ ],
278
+ textPatterns: [/newsletter/i, /abonnieren/i, /subscribe/i],
279
+ ariaRoles: [],
280
+ minSize: { width: 200, height: 80 },
281
+ padding: 20,
282
+ scrollTo: true,
283
+ },
284
+ ];
285
+ // ─── Main Function ────────────────────────────────────────────────────
286
+ export async function smartScreenshot(config) {
287
+ const { url, targets, outputDir = path.join(OUTPUT_DIR, 'smart-screenshots'), viewport = { width: 1920, height: 1080 }, deviceScaleFactor = 1, darkMode = false, waitAfterLoad = 2000, includeFullPage = false, maxWidth = 1920, maxHeight = 2000, } = config;
288
+ if (!fs.existsSync(outputDir)) {
289
+ fs.mkdirSync(outputDir, { recursive: true });
290
+ }
291
+ const startTime = Date.now();
292
+ let browser;
293
+ try {
294
+ logger.info(`Smart Screenshot: ${url}`);
295
+ logger.info(`Targets: ${targets.map(t => typeof t === 'string' ? t : t.feature).join(', ')}`);
296
+ // ─── 1. Launch Browser ──────────────────────────────────────
297
+ browser = await chromium.launch({
298
+ headless: true,
299
+ args: [
300
+ '--no-sandbox',
301
+ '--disable-setuid-sandbox',
302
+ '--disable-dev-shm-usage',
303
+ '--disable-gpu',
304
+ '--hide-scrollbars',
305
+ ],
306
+ });
307
+ const context = await browser.newContext({
308
+ viewport,
309
+ deviceScaleFactor,
310
+ colorScheme: darkMode ? 'dark' : 'light',
311
+ });
312
+ const page = await context.newPage();
313
+ // ─── 2. Navigate ────────────────────────────────────────────
314
+ logger.info(`Navigating to ${url}...`);
315
+ await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 }).catch(async () => {
316
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
317
+ });
318
+ await page.waitForTimeout(waitAfterLoad);
319
+ // ─── 3. Dismiss overlays ────────────────────────────────────
320
+ await dismissOverlays(page);
321
+ await page.waitForTimeout(500);
322
+ // ─── 4. Pre-scroll to trigger lazy loading ──────────────────
323
+ await preloadContent(page, viewport.height);
324
+ // Hide scrollbar
325
+ await page.addStyleTag({
326
+ content: `::-webkit-scrollbar { display: none !important; } * { scrollbar-width: none !important; }`,
327
+ });
328
+ // ─── 5. Detect features ─────────────────────────────────────
329
+ const normalizedTargets = normalizeTargets(targets);
330
+ const isAutoDetect = normalizedTargets.some(t => t.feature === 'all');
331
+ let detectedFeatures;
332
+ if (isAutoDetect) {
333
+ logger.info('Auto-detecting all page features...');
334
+ detectedFeatures = await detectAllFeatures(page, viewport);
335
+ }
336
+ else {
337
+ detectedFeatures = [];
338
+ for (const target of normalizedTargets) {
339
+ const features = await detectFeature(page, target, viewport);
340
+ detectedFeatures.push(...features);
341
+ }
342
+ }
343
+ logger.info(`Detected ${detectedFeatures.length} features`);
344
+ for (const f of detectedFeatures) {
345
+ logger.info(` ${f.name}: ${f.bounds.width}x${f.bounds.height} (${f.matchMethod}, ${f.confidence})`);
346
+ }
347
+ // ─── 6. Take screenshots ────────────────────────────────────
348
+ const screenshots = [];
349
+ const domain = new URL(url).hostname.replace(/^www\./, '').replace(/\./g, '-');
350
+ for (let i = 0; i < detectedFeatures.length; i++) {
351
+ const feature = detectedFeatures[i];
352
+ const safeName = feature.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+/g, '-');
353
+ const ssPath = path.join(outputDir, `${domain}-${safeName}-${i}.png`);
354
+ try {
355
+ // Scroll element into view
356
+ await page.evaluate((sel) => {
357
+ const el = document.querySelector(sel);
358
+ if (el) {
359
+ el.scrollIntoView({ block: 'center', behavior: 'instant' });
360
+ }
361
+ }, feature.selector);
362
+ await page.waitForTimeout(300);
363
+ // Get VIEWPORT-RELATIVE bounds (getBoundingClientRect without scrollY)
364
+ const vpBounds = await page.evaluate((sel) => {
365
+ const el = document.querySelector(sel);
366
+ if (!el)
367
+ return null;
368
+ const rect = el.getBoundingClientRect();
369
+ return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
370
+ }, feature.selector);
371
+ if (!vpBounds || vpBounds.width < 10 || vpBounds.height < 10) {
372
+ logger.info(` Skipping ${feature.name}: element not found after scroll`);
373
+ continue;
374
+ }
375
+ // Apply padding
376
+ const padding = feature.pattern === 'custom' ? 20 : (findPattern(feature.pattern)?.padding ?? 20);
377
+ const clip = {
378
+ x: Math.max(0, vpBounds.x - padding),
379
+ y: Math.max(0, vpBounds.y - padding),
380
+ width: Math.min(vpBounds.width + padding * 2, maxWidth),
381
+ height: Math.min(vpBounds.height + padding * 2, maxHeight),
382
+ };
383
+ // Ensure clip doesn't exceed viewport dimensions
384
+ if (clip.x + clip.width > viewport.width) {
385
+ clip.width = viewport.width - clip.x;
386
+ }
387
+ if (clip.y + clip.height > viewport.height) {
388
+ clip.height = viewport.height - clip.y;
389
+ }
390
+ // Skip if clip is too small or invalid
391
+ if (clip.width < 20 || clip.height < 20) {
392
+ logger.info(` Skipping ${feature.name}: clipped area too small`);
393
+ continue;
394
+ }
395
+ await page.screenshot({
396
+ path: ssPath,
397
+ type: 'png',
398
+ clip,
399
+ });
400
+ screenshots.push({
401
+ feature: feature.name,
402
+ path: ssPath,
403
+ bounds: clip,
404
+ matchMethod: feature.matchMethod,
405
+ confidence: feature.confidence,
406
+ });
407
+ logger.info(` Screenshot: ${feature.name} → ${path.basename(ssPath)}`);
408
+ }
409
+ catch (err) {
410
+ logger.error(` Failed to screenshot ${feature.name}: ${err instanceof Error ? err.message : String(err)}`);
411
+ }
412
+ }
413
+ // ─── 7. Full-page screenshot (optional) ─────────────────────
414
+ let fullPagePath;
415
+ if (includeFullPage) {
416
+ fullPagePath = path.join(outputDir, `${domain}-full-page.png`);
417
+ await page.evaluate(() => window.scrollTo(0, 0));
418
+ await page.waitForTimeout(300);
419
+ await page.screenshot({ path: fullPagePath, fullPage: true, type: 'png' });
420
+ logger.info(` Full-page screenshot → ${path.basename(fullPagePath)}`);
421
+ }
422
+ // ─── 8. Cleanup ─────────────────────────────────────────────
423
+ await context.close();
424
+ await browser.close();
425
+ browser = undefined;
426
+ const totalTime = `${((Date.now() - startTime) / 1000).toFixed(1)}s`;
427
+ logger.info(`Smart Screenshot complete: ${screenshots.length} captures in ${totalTime}`);
428
+ return {
429
+ success: true,
430
+ url,
431
+ screenshots,
432
+ detected: detectedFeatures,
433
+ fullPage: fullPagePath,
434
+ totalTime,
435
+ };
436
+ }
437
+ catch (error) {
438
+ if (browser) {
439
+ try {
440
+ await browser.close();
441
+ }
442
+ catch { /* ignore */ }
443
+ }
444
+ const message = error instanceof Error ? error.message : String(error);
445
+ logger.error(`Smart Screenshot failed: ${message}`);
446
+ throw new Error(`Smart Screenshot failed: ${message}`);
447
+ }
448
+ }
449
+ // ─── Feature Detection ────────────────────────────────────────────────
450
+ function normalizeTargets(targets) {
451
+ return targets.map(t => {
452
+ if (typeof t === 'string') {
453
+ return { feature: t.toLowerCase().trim() };
454
+ }
455
+ return { ...t, feature: t.feature.toLowerCase().trim() };
456
+ });
457
+ }
458
+ function findPattern(keyword) {
459
+ return FEATURE_PATTERNS.find(p => p.keywords.some(k => k === keyword) ||
460
+ p.name.toLowerCase() === keyword);
461
+ }
462
+ function findPatternsByKeyword(keyword) {
463
+ // Exact match first
464
+ const exact = FEATURE_PATTERNS.filter(p => p.keywords.some(k => k === keyword) ||
465
+ p.name.toLowerCase() === keyword);
466
+ if (exact.length > 0)
467
+ return exact;
468
+ // Fuzzy match: keyword is a substring of a pattern keyword or vice versa
469
+ return FEATURE_PATTERNS.filter(p => p.keywords.some(k => k.includes(keyword) || keyword.includes(k)) ||
470
+ p.name.toLowerCase().includes(keyword) ||
471
+ keyword.includes(p.name.toLowerCase()));
472
+ }
473
+ async function detectFeature(page, target, viewport) {
474
+ const results = [];
475
+ // Custom selector override
476
+ if (target.selector) {
477
+ const bounds = await getElementBounds(page, target.selector);
478
+ if (bounds && bounds.width > 10 && bounds.height > 10) {
479
+ results.push({
480
+ name: target.feature || 'Custom Element',
481
+ pattern: 'custom',
482
+ selector: target.selector,
483
+ bounds,
484
+ matchMethod: 'custom',
485
+ confidence: 'high',
486
+ });
487
+ }
488
+ return results;
489
+ }
490
+ // Feature keyword starts with . or # or [ → treat as CSS selector
491
+ if (/^[.#\[]/.test(target.feature)) {
492
+ const bounds = await getElementBounds(page, target.feature);
493
+ if (bounds && bounds.width > 10 && bounds.height > 10) {
494
+ results.push({
495
+ name: target.feature,
496
+ pattern: 'custom',
497
+ selector: target.feature,
498
+ bounds,
499
+ matchMethod: 'custom',
500
+ confidence: 'high',
501
+ });
502
+ }
503
+ return results;
504
+ }
505
+ // Click to reveal if needed (e.g. chat widget)
506
+ const patterns = findPatternsByKeyword(target.feature);
507
+ if (patterns.length === 0) {
508
+ logger.info(` No pattern found for "${target.feature}", trying as text search...`);
509
+ const textResults = await findByVisibleText(page, target.feature, viewport);
510
+ results.push(...textResults);
511
+ return results;
512
+ }
513
+ for (const pattern of patterns) {
514
+ // Try reveal selector first (e.g. click to open chat)
515
+ if ((target.revealFirst || pattern.revealSelector) && pattern.revealSelector) {
516
+ try {
517
+ const revealBtn = page.locator(pattern.revealSelector).first();
518
+ if (await revealBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
519
+ await revealBtn.click({ timeout: 2000 });
520
+ await page.waitForTimeout(1000);
521
+ logger.info(` Clicked reveal button for ${pattern.name}`);
522
+ }
523
+ }
524
+ catch { /* no reveal button found, continue */ }
525
+ }
526
+ // Try CSS selectors
527
+ for (const selector of pattern.selectors) {
528
+ const bounds = await getElementBounds(page, selector);
529
+ if (bounds && meetsMinSize(bounds, pattern.minSize)) {
530
+ results.push({
531
+ name: pattern.name,
532
+ pattern: pattern.keywords[0],
533
+ selector,
534
+ bounds,
535
+ matchMethod: 'selector',
536
+ confidence: 'high',
537
+ });
538
+ break; // Found via selector, no need to check more
539
+ }
540
+ }
541
+ // If not found by selector, try text patterns
542
+ if (results.filter(r => r.pattern === pattern.keywords[0]).length === 0) {
543
+ for (const textPattern of pattern.textPatterns) {
544
+ const textResults = await findSectionByText(page, textPattern, pattern.name, pattern.keywords[0], viewport);
545
+ results.push(...textResults);
546
+ if (textResults.length > 0)
547
+ break;
548
+ }
549
+ }
550
+ // Try ARIA roles
551
+ if (results.filter(r => r.pattern === pattern.keywords[0]).length === 0) {
552
+ for (const role of pattern.ariaRoles) {
553
+ const ariaSelector = `[role="${role}"]`;
554
+ const bounds = await getElementBounds(page, ariaSelector);
555
+ if (bounds && meetsMinSize(bounds, pattern.minSize)) {
556
+ results.push({
557
+ name: pattern.name,
558
+ pattern: pattern.keywords[0],
559
+ selector: ariaSelector,
560
+ bounds,
561
+ matchMethod: 'aria',
562
+ confidence: 'medium',
563
+ });
564
+ break;
565
+ }
566
+ }
567
+ }
568
+ }
569
+ return results;
570
+ }
571
+ async function detectAllFeatures(page, viewport) {
572
+ const results = [];
573
+ const seenBounds = new Set();
574
+ for (const pattern of FEATURE_PATTERNS) {
575
+ // Try each selector
576
+ for (const selector of pattern.selectors) {
577
+ const bounds = await getElementBounds(page, selector);
578
+ if (bounds && meetsMinSize(bounds, pattern.minSize)) {
579
+ const boundsKey = `${Math.round(bounds.x)}-${Math.round(bounds.y)}-${Math.round(bounds.width)}-${Math.round(bounds.height)}`;
580
+ if (!seenBounds.has(boundsKey)) {
581
+ seenBounds.add(boundsKey);
582
+ results.push({
583
+ name: pattern.name,
584
+ pattern: pattern.keywords[0],
585
+ selector,
586
+ bounds,
587
+ matchMethod: 'selector',
588
+ confidence: 'high',
589
+ });
590
+ break; // One match per pattern
591
+ }
592
+ }
593
+ }
594
+ }
595
+ return results;
596
+ }
597
+ async function findByVisibleText(page, searchText, viewport) {
598
+ const results = [];
599
+ const regex = new RegExp(searchText, 'i');
600
+ const found = await findSectionByText(page, regex, searchText, searchText, viewport);
601
+ results.push(...found);
602
+ return results;
603
+ }
604
+ async function findSectionByText(page, textPattern, featureName, patternKey, _viewport) {
605
+ const results = [];
606
+ // Find the nearest section/container that contains this text
607
+ const element = await page.evaluate((pattern) => {
608
+ const re = new RegExp(pattern, 'i');
609
+ // Search headings first, then paragraphs, then sections
610
+ const candidates = [
611
+ ...Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6')),
612
+ ...Array.from(document.querySelectorAll('section, [class*="section"], article')),
613
+ ...Array.from(document.querySelectorAll('p, span, div')),
614
+ ];
615
+ for (const el of candidates) {
616
+ const text = el.textContent?.trim() ?? '';
617
+ if (re.test(text)) {
618
+ // Walk up to find a meaningful container
619
+ let container = el;
620
+ for (let i = 0; i < 5; i++) {
621
+ const parent = container.parentElement;
622
+ if (!parent || parent === document.body || parent === document.documentElement)
623
+ break;
624
+ const tag = parent.tagName.toLowerCase();
625
+ if (tag === 'section' || tag === 'article' || tag === 'main' ||
626
+ parent.classList.toString().match(/section|container|wrapper|block|card/i)) {
627
+ container = parent;
628
+ break;
629
+ }
630
+ container = parent;
631
+ }
632
+ const rect = container.getBoundingClientRect();
633
+ if (rect.width > 100 && rect.height > 50) {
634
+ // Generate a unique selector for this element
635
+ let sel = container.tagName.toLowerCase();
636
+ if (container.id)
637
+ sel = `#${container.id}`;
638
+ else if (container.className && typeof container.className === 'string') {
639
+ const cls = container.className.split(/\s+/).filter(c => c.length > 0 && !c.includes(':'))[0];
640
+ if (cls)
641
+ sel = `.${cls}`;
642
+ }
643
+ return {
644
+ selector: sel,
645
+ bounds: {
646
+ x: rect.x + window.scrollX,
647
+ y: rect.y + window.scrollY,
648
+ width: rect.width,
649
+ height: rect.height,
650
+ },
651
+ };
652
+ }
653
+ }
654
+ }
655
+ return null;
656
+ }, textPattern.source);
657
+ if (element) {
658
+ results.push({
659
+ name: featureName,
660
+ pattern: patternKey,
661
+ selector: element.selector,
662
+ bounds: element.bounds,
663
+ matchMethod: 'text',
664
+ confidence: 'medium',
665
+ });
666
+ }
667
+ return results;
668
+ }
669
+ // ─── DOM Helpers ──────────────────────────────────────────────────────
670
+ async function getElementBounds(page, selector) {
671
+ try {
672
+ return await page.evaluate((sel) => {
673
+ const el = document.querySelector(sel);
674
+ if (!el)
675
+ return null;
676
+ const rect = el.getBoundingClientRect();
677
+ return {
678
+ x: rect.x + window.scrollX,
679
+ y: rect.y + window.scrollY,
680
+ width: rect.width,
681
+ height: rect.height,
682
+ };
683
+ }, selector);
684
+ }
685
+ catch {
686
+ return null;
687
+ }
688
+ }
689
+ function meetsMinSize(bounds, minSize) {
690
+ if (!minSize)
691
+ return bounds.width > 10 && bounds.height > 10;
692
+ return bounds.width >= minSize.width && bounds.height >= minSize.height;
693
+ }
694
+ // ─── Page Preparation ─────────────────────────────────────────────────
695
+ async function dismissOverlays(page) {
696
+ // Set consent cookies
697
+ await page.evaluate(() => {
698
+ localStorage.setItem('cookie-consent', 'accepted');
699
+ localStorage.setItem('cookieConsent', 'accepted');
700
+ localStorage.setItem('cookie_consent', 'true');
701
+ localStorage.setItem('cookies-accepted', 'true');
702
+ localStorage.setItem('gdpr-consent', 'true');
703
+ localStorage.setItem('CookieConsent', 'true');
704
+ document.cookie = 'cookie-consent=accepted; path=/; max-age=31536000';
705
+ window.dispatchEvent(new Event('cookie-consent-accepted'));
706
+ });
707
+ await page.waitForTimeout(300);
708
+ // Click accept buttons
709
+ for (const text of ['Akzeptieren', 'Accept', 'Alle akzeptieren', 'Accept all', 'OK', 'Verstanden']) {
710
+ try {
711
+ const btn = page.locator(`button:has-text("${text}")`).first();
712
+ if (await btn.isVisible({ timeout: 500 }).catch(() => false)) {
713
+ await btn.click({ timeout: 1000 });
714
+ break;
715
+ }
716
+ }
717
+ catch { /* next */ }
718
+ }
719
+ // Force-hide common overlay selectors
720
+ await page.addStyleTag({
721
+ content: `
722
+ [class*="cookie"], [class*="Cookie"],
723
+ [class*="consent"], [class*="Consent"],
724
+ [id*="cookie"], [id*="consent"],
725
+ [role="dialog"],
726
+ .fixed.bottom-0.left-0.right-0.z-50 {
727
+ display: none !important;
728
+ visibility: hidden !important;
729
+ }
730
+ `,
731
+ });
732
+ }
733
+ async function preloadContent(page, viewportHeight) {
734
+ const scrollHeight = await page.evaluate(() => document.documentElement.scrollHeight);
735
+ const steps = Math.ceil(scrollHeight / (viewportHeight * 0.7));
736
+ for (let i = 0; i <= steps; i++) {
737
+ const y = Math.min(i * viewportHeight * 0.7, scrollHeight);
738
+ await page.evaluate((scrollY) => window.scrollTo(0, scrollY), y);
739
+ await page.waitForTimeout(150);
740
+ }
741
+ await page.evaluate(() => window.scrollTo(0, 0));
742
+ await page.waitForTimeout(400);
743
+ }
744
+ //# sourceMappingURL=smart-screenshot.js.map