@veolab/discoverylab 1.3.3 → 1.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 (37) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/dist/chunk-34KRJWZL.js +477 -0
  4. package/dist/chunk-4VNS5WPM.js +42 -0
  5. package/dist/{chunk-FIL7IWEL.js → chunk-DGXAP477.js} +1 -1
  6. package/dist/{chunk-HGWEHWKJ.js → chunk-DKAX5RCX.js} +1 -1
  7. package/dist/{chunk-7EDIUVIO.js → chunk-EU63HPKT.js} +1 -1
  8. package/dist/chunk-QMUEC6B5.js +288 -0
  9. package/dist/{chunk-FNUN7EPB.js → chunk-RCY26WEK.js} +2 -2
  10. package/dist/{chunk-ZLHIHMSL.js → chunk-SWZIBO2R.js} +1 -1
  11. package/dist/chunk-VYYAP5G5.js +265 -0
  12. package/dist/{chunk-VVIOB362.js → chunk-XAMA3JJG.js} +18 -1
  13. package/dist/{chunk-LXSWDEXV.js → chunk-XWBFSSNB.js} +10224 -389
  14. package/dist/{chunk-AHVBE25Y.js → chunk-YNLUOZSZ.js} +274 -667
  15. package/dist/cli.js +33 -31
  16. package/dist/{db-6WLEVKUV.js → db-745LC5YC.js} +2 -2
  17. package/dist/document-AE4XI2CP.js +104 -0
  18. package/dist/{esvp-KVOWYW6G.js → esvp-4LIAU76K.js} +3 -3
  19. package/dist/{esvp-mobile-GZ5EMYPG.js → esvp-mobile-FKFHDS5Q.js} +4 -4
  20. package/dist/frames-RCNLSDD6.js +24 -0
  21. package/dist/{gridCompositor-M3K3LCLZ.js → gridCompositor-VUWBZXYL.js} +262 -3
  22. package/dist/index.d.ts +32 -0
  23. package/dist/index.html +1197 -9
  24. package/dist/index.js +15 -10
  25. package/dist/notion-api-OXSWOJPZ.js +190 -0
  26. package/dist/{ocr-QDYNCSPE.js → ocr-FXRLEP66.js} +1 -1
  27. package/dist/{playwright-VZ7PXDC5.js → playwright-GYKUH34L.js} +3 -3
  28. package/dist/renderer-D22GCMMD.js +17 -0
  29. package/dist/{server-6N3KIEGP.js → server-NTT2XGCC.js} +1 -1
  30. package/dist/server-TKYRIYJ6.js +24 -0
  31. package/dist/{setup-2SQC5UHJ.js → setup-O6WQQAGP.js} +3 -3
  32. package/dist/templates/bundle/bundle.js +4 -2
  33. package/dist/{tools-YGM5HRIB.js → tools-FVVWKEGC.js} +15 -7
  34. package/package.json +2 -2
  35. package/skills/knowledge-brain/SKILL.md +64 -0
  36. package/dist/chunk-MLKGABMK.js +0 -9
  37. package/dist/server-T5X6GGOO.js +0 -22
@@ -0,0 +1,288 @@
1
+ import {
2
+ FRAMES_DIR
3
+ } from "./chunk-XAMA3JJG.js";
4
+ import {
5
+ __require
6
+ } from "./chunk-4VNS5WPM.js";
7
+
8
+ // src/core/analyze/frames.ts
9
+ import { spawn, execSync } from "child_process";
10
+ import { existsSync, mkdirSync, readdirSync, statSync } from "fs";
11
+ import { join, basename } from "path";
12
+ function getVideoInfo(videoPath) {
13
+ if (!existsSync(videoPath)) {
14
+ return null;
15
+ }
16
+ try {
17
+ const output = execSync(
18
+ `ffprobe -v quiet -print_format json -show_format -show_streams "${videoPath}"`,
19
+ { encoding: "utf-8" }
20
+ );
21
+ const data = JSON.parse(output);
22
+ const videoStream = data.streams?.find((s) => s.codec_type === "video");
23
+ if (!videoStream) {
24
+ return null;
25
+ }
26
+ let fps = 30;
27
+ if (videoStream.r_frame_rate) {
28
+ const [num, den] = videoStream.r_frame_rate.split("/");
29
+ fps = den ? parseInt(num, 10) / parseInt(den, 10) : parseFloat(num);
30
+ }
31
+ const duration = parseFloat(data.format?.duration || videoStream.duration || "0");
32
+ const frameCount = parseInt(videoStream.nb_frames, 10) || Math.round(duration * fps);
33
+ return {
34
+ duration,
35
+ frameCount,
36
+ fps,
37
+ width: videoStream.width,
38
+ height: videoStream.height,
39
+ codec: videoStream.codec_name
40
+ };
41
+ } catch (error) {
42
+ console.error("Failed to get video info:", error);
43
+ return null;
44
+ }
45
+ }
46
+ async function extractFrames(options) {
47
+ const { projectId, videoPath, outputFormat = "png" } = options;
48
+ if (!existsSync(videoPath)) {
49
+ return { success: false, error: `Video file not found: ${videoPath}` };
50
+ }
51
+ const framesDir = join(FRAMES_DIR, projectId);
52
+ if (!existsSync(framesDir)) {
53
+ mkdirSync(framesDir, { recursive: true });
54
+ }
55
+ const videoInfo = getVideoInfo(videoPath);
56
+ if (!videoInfo) {
57
+ return { success: false, error: "Failed to read video information" };
58
+ }
59
+ const args = buildFFmpegArgs(options, framesDir, videoInfo, outputFormat);
60
+ return new Promise((resolve) => {
61
+ const proc = spawn("ffmpeg", args);
62
+ let stderr = "";
63
+ proc.stderr.on("data", (data) => {
64
+ stderr += data.toString();
65
+ });
66
+ proc.on("close", (code) => {
67
+ if (code !== 0) {
68
+ resolve({ success: false, error: `FFmpeg failed: ${stderr.slice(-500)}` });
69
+ return;
70
+ }
71
+ const frames = listExtractedFrames(framesDir, videoInfo.fps, options.fps || 1);
72
+ resolve({
73
+ success: true,
74
+ framesDir,
75
+ frameCount: frames.length,
76
+ frames
77
+ });
78
+ });
79
+ proc.on("error", (err) => {
80
+ resolve({ success: false, error: err.message });
81
+ });
82
+ });
83
+ }
84
+ function buildFFmpegArgs(options, framesDir, videoInfo, outputFormat) {
85
+ const args = ["-y"];
86
+ args.push("-i", options.videoPath);
87
+ if (options.startTime !== void 0) {
88
+ args.push("-ss", options.startTime.toString());
89
+ }
90
+ if (options.endTime !== void 0) {
91
+ args.push("-to", options.endTime.toString());
92
+ }
93
+ if (options.keyFramesOnly) {
94
+ args.push("-vf", "select=eq(pict_type\\,I)");
95
+ args.push("-vsync", "vfr");
96
+ } else if (options.fps) {
97
+ args.push("-vf", `fps=${options.fps}`);
98
+ } else {
99
+ args.push("-vf", "fps=1");
100
+ }
101
+ if (options.maxFrames) {
102
+ args.push("-frames:v", options.maxFrames.toString());
103
+ }
104
+ if (outputFormat === "jpg" && options.quality) {
105
+ args.push("-q:v", options.quality.toString());
106
+ }
107
+ const outputPattern = join(framesDir, `frame-%04d.${outputFormat}`);
108
+ args.push(outputPattern);
109
+ return args;
110
+ }
111
+ function listExtractedFrames(framesDir, videoFps, extractFps) {
112
+ const files = readdirSync(framesDir).filter((f) => f.startsWith("frame-") && (f.endsWith(".png") || f.endsWith(".jpg"))).sort();
113
+ return files.map((filename, index) => {
114
+ const timestamp = index / extractFps;
115
+ const match = filename.match(/frame-(\d+)/);
116
+ const frameNumber = match ? parseInt(match[1], 10) : index + 1;
117
+ return {
118
+ path: join(framesDir, filename),
119
+ frameNumber,
120
+ timestamp,
121
+ filename
122
+ };
123
+ });
124
+ }
125
+ function isBlankFrame(imagePath) {
126
+ try {
127
+ const output = execSync(
128
+ `ffmpeg -i "${imagePath}" -vf "crop=iw*0.8:ih*0.8:iw*0.1:ih*0.1,signalstats" -f null - 2>&1`,
129
+ { encoding: "utf-8", timeout: 5e3 }
130
+ );
131
+ const match = output.match(/YAVG:\s*([\d.]+)/);
132
+ if (!match) return { isBlank: false, brightness: 128 };
133
+ const brightness = parseFloat(match[1]);
134
+ return {
135
+ isBlank: brightness > 240 || brightness < 15,
136
+ brightness
137
+ };
138
+ } catch {
139
+ return { isBlank: false, brightness: 128 };
140
+ }
141
+ }
142
+ function filterBlankFrames(frames) {
143
+ const { unlinkSync } = __require("fs");
144
+ return frames.filter((frame) => {
145
+ if (!existsSync(frame.path)) return false;
146
+ const { isBlank } = isBlankFrame(frame.path);
147
+ if (isBlank) {
148
+ try {
149
+ unlinkSync(frame.path);
150
+ } catch {
151
+ }
152
+ return false;
153
+ }
154
+ return true;
155
+ });
156
+ }
157
+ async function detectKeyFrames(videoPath, threshold = 0.3) {
158
+ if (!existsSync(videoPath)) {
159
+ return [];
160
+ }
161
+ try {
162
+ const output = execSync(
163
+ `ffmpeg -i "${videoPath}" -vf "select='gt(scene,${threshold})',showinfo" -f null - 2>&1`,
164
+ { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }
165
+ );
166
+ const keyFrames = [];
167
+ const lines = output.split("\n");
168
+ for (const line of lines) {
169
+ const match = line.match(/n:\s*(\d+).*pts_time:\s*([\d.]+)/);
170
+ if (match) {
171
+ const frameNumber = parseInt(match[1], 10);
172
+ const timestamp = parseFloat(match[2]);
173
+ const scoreMatch = line.match(/scene:\s*([\d.]+)/);
174
+ const score = scoreMatch ? parseFloat(scoreMatch[1]) : threshold;
175
+ keyFrames.push({
176
+ path: "",
177
+ frameNumber,
178
+ timestamp,
179
+ filename: `keyframe-${frameNumber}.png`,
180
+ score,
181
+ isSceneChange: score > threshold
182
+ });
183
+ }
184
+ }
185
+ return keyFrames;
186
+ } catch (error) {
187
+ console.error("Failed to detect key frames:", error);
188
+ return [];
189
+ }
190
+ }
191
+ async function extractKeyFramesOnly(projectId, videoPath, threshold = 0.3) {
192
+ const keyFrames = await detectKeyFrames(videoPath, threshold);
193
+ if (keyFrames.length === 0) {
194
+ return extractFrames({
195
+ projectId,
196
+ videoPath,
197
+ fps: 1,
198
+ maxFrames: 30
199
+ });
200
+ }
201
+ const framesDir = join(FRAMES_DIR, projectId);
202
+ if (!existsSync(framesDir)) {
203
+ mkdirSync(framesDir, { recursive: true });
204
+ }
205
+ const extractedFrames = [];
206
+ for (let i = 0; i < keyFrames.length; i++) {
207
+ const kf = keyFrames[i];
208
+ const outputPath = join(framesDir, `keyframe-${String(i + 1).padStart(4, "0")}.png`);
209
+ try {
210
+ execSync(
211
+ `ffmpeg -y -ss ${kf.timestamp} -i "${videoPath}" -frames:v 1 "${outputPath}"`,
212
+ { stdio: "ignore" }
213
+ );
214
+ if (existsSync(outputPath)) {
215
+ extractedFrames.push({
216
+ path: outputPath,
217
+ frameNumber: i + 1,
218
+ timestamp: kf.timestamp,
219
+ filename: basename(outputPath)
220
+ });
221
+ }
222
+ } catch {
223
+ }
224
+ }
225
+ return {
226
+ success: true,
227
+ framesDir,
228
+ frameCount: extractedFrames.length,
229
+ frames: extractedFrames
230
+ };
231
+ }
232
+ async function generateThumbnail(videoPath, outputPath, timestamp = 0, width = 320) {
233
+ if (!existsSync(videoPath)) {
234
+ return false;
235
+ }
236
+ try {
237
+ execSync(
238
+ `ffmpeg -y -ss ${timestamp} -i "${videoPath}" -vframes 1 -vf "scale=${width}:-1" "${outputPath}"`,
239
+ { stdio: "ignore" }
240
+ );
241
+ return existsSync(outputPath);
242
+ } catch {
243
+ return false;
244
+ }
245
+ }
246
+ function compareFrames(frame1, frame2) {
247
+ try {
248
+ const output = execSync(
249
+ `compare -metric RMSE "${frame1}" "${frame2}" null: 2>&1 || true`,
250
+ { encoding: "utf-8" }
251
+ );
252
+ const match = output.match(/^([\d.]+)/);
253
+ if (match) {
254
+ return parseFloat(match[1]);
255
+ }
256
+ return null;
257
+ } catch {
258
+ return null;
259
+ }
260
+ }
261
+ function cleanupFrames(projectId) {
262
+ const framesDir = join(FRAMES_DIR, projectId);
263
+ if (existsSync(framesDir)) {
264
+ const files = readdirSync(framesDir);
265
+ for (const file of files) {
266
+ try {
267
+ const filePath = join(framesDir, file);
268
+ const stat = statSync(filePath);
269
+ if (stat.isFile()) {
270
+ __require("fs").unlinkSync(filePath);
271
+ }
272
+ } catch {
273
+ }
274
+ }
275
+ }
276
+ }
277
+
278
+ export {
279
+ getVideoInfo,
280
+ extractFrames,
281
+ isBlankFrame,
282
+ filterBlankFrames,
283
+ detectKeyFrames,
284
+ extractKeyFramesOnly,
285
+ generateThumbnail,
286
+ compareFrames,
287
+ cleanupFrames
288
+ };
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  DATA_DIR,
7
7
  DB_PATH
8
- } from "./chunk-VVIOB362.js";
8
+ } from "./chunk-XAMA3JJG.js";
9
9
 
10
10
  // src/mcp/tools/setup.ts
11
11
  import { z } from "zod";
@@ -224,7 +224,7 @@ var setupInitTool = {
224
224
  inputSchema: z.object({}),
225
225
  handler: async () => {
226
226
  try {
227
- const { getDatabase, DATA_DIR: DATA_DIR2, PROJECTS_DIR, EXPORTS_DIR, FRAMES_DIR } = await import("./db-6WLEVKUV.js");
227
+ const { getDatabase, DATA_DIR: DATA_DIR2, PROJECTS_DIR, EXPORTS_DIR, FRAMES_DIR } = await import("./db-745LC5YC.js");
228
228
  getDatabase();
229
229
  return createJsonResult({
230
230
  message: "DiscoveryLab initialized successfully",
@@ -15,7 +15,7 @@ import {
15
15
  inspectESVPSession,
16
16
  replayESVPSession,
17
17
  runESVPActions
18
- } from "./chunk-7EDIUVIO.js";
18
+ } from "./chunk-EU63HPKT.js";
19
19
  import {
20
20
  redactSensitiveTestInput
21
21
  } from "./chunk-SLNJEF32.js";
@@ -0,0 +1,265 @@
1
+ import {
2
+ getBundlePath,
3
+ getTemplate
4
+ } from "./chunk-DKAX5RCX.js";
5
+ import {
6
+ DATA_DIR,
7
+ EXPORTS_DIR
8
+ } from "./chunk-XAMA3JJG.js";
9
+
10
+ // src/core/templates/renderer.ts
11
+ import { existsSync, mkdirSync } from "fs";
12
+ import { execSync } from "child_process";
13
+ import { join } from "path";
14
+ var renderJobs = /* @__PURE__ */ new Map();
15
+ var jobCounter = 0;
16
+ function generateJobId() {
17
+ return `render_${Date.now()}_${++jobCounter}`;
18
+ }
19
+ function getOutputDir(projectId) {
20
+ const dir = join(EXPORTS_DIR, projectId);
21
+ if (!existsSync(dir)) {
22
+ mkdirSync(dir, { recursive: true });
23
+ }
24
+ return dir;
25
+ }
26
+ function getLineCount(text) {
27
+ return String(text || "").replace(/\r\n/g, "\n").split("\n").length;
28
+ }
29
+ function shouldPreserveTerminalTabContent(tab, profile) {
30
+ return tab.content.length <= profile.maxCharsPerTab && getLineCount(tab.content) <= profile.maxLinesPerTab;
31
+ }
32
+ function clampTextForTerminalRender(text, profile) {
33
+ const normalized = String(text || "").replace(/\r\n/g, "\n").replace(/\t/g, " ");
34
+ const rawLines = normalized.split("\n").map((line) => line.replace(/\s+$/g, ""));
35
+ const truncatedByLines = rawLines.length > profile.maxLinesPerTab;
36
+ const visibleLines = truncatedByLines ? rawLines.slice(0, profile.maxLinesPerTab - 1) : rawLines.slice(0, profile.maxLinesPerTab);
37
+ if (truncatedByLines) {
38
+ const hiddenLines = Math.max(1, rawLines.length - visibleLines.length);
39
+ visibleLines.push(`... +${hiddenLines} more lines`);
40
+ }
41
+ let output = visibleLines.join("\n").trim();
42
+ if (output.length > profile.maxCharsPerTab) {
43
+ output = `${output.slice(0, Math.max(0, profile.maxCharsPerTab - 12)).trimEnd()}
44
+ ...`;
45
+ }
46
+ return output || "// no terminal content";
47
+ }
48
+ function formatCompactBytes(value) {
49
+ const bytes = Number(value);
50
+ if (!Number.isFinite(bytes) || bytes <= 0) return "";
51
+ if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(1)}MB`;
52
+ if (bytes >= 1e3) return `${(bytes / 1e3).toFixed(1)}KB`;
53
+ return `${Math.round(bytes)}B`;
54
+ }
55
+ function compactTerminalJsonContent(tab, profile) {
56
+ try {
57
+ const parsed = JSON.parse(tab.content);
58
+ if (!Array.isArray(parsed)) return null;
59
+ const lines = parsed.slice(0, profile.maxJsonItemsPerTab).map((item) => {
60
+ if (!item || typeof item !== "object") return null;
61
+ const record = item;
62
+ const status = record.status ?? "...";
63
+ const duration = Number.isFinite(Number(record.durationMs)) ? `${Math.round(Number(record.durationMs))}ms` : "";
64
+ const size = formatCompactBytes(record.responseSize);
65
+ const rawUrl = typeof record.url === "string" ? record.url : "";
66
+ let displayUrl = rawUrl || tab.route || tab.label;
67
+ try {
68
+ const parsedUrl = new URL(rawUrl);
69
+ displayUrl = parsedUrl.pathname || `${parsedUrl.origin}${parsedUrl.pathname}`;
70
+ } catch {
71
+ }
72
+ return [status, duration, size, displayUrl].filter(Boolean).join(" ");
73
+ }).filter((line) => !!line);
74
+ if (parsed.length > lines.length) {
75
+ lines.push(`... +${parsed.length - lines.length} more requests`);
76
+ }
77
+ return clampTextForTerminalRender(lines.join("\n"), profile);
78
+ } catch {
79
+ return null;
80
+ }
81
+ }
82
+ function getTemplateRenderOptimizationProfile(templateId, props, realVideoDuration) {
83
+ const totalChars = Array.isArray(props.terminalTabs) ? props.terminalTabs.reduce((sum, tab) => sum + String(tab?.content || "").length, 0) : 0;
84
+ const tabCount = Array.isArray(props.terminalTabs) ? props.terminalTabs.length : 0;
85
+ const isLongVideo = (realVideoDuration || props.videoDuration || 0) >= 30;
86
+ const isHeavyTerminal = totalChars >= 1600 || tabCount >= 6;
87
+ const showcaseAggressive = templateId === "showcase";
88
+ if (isLongVideo || isHeavyTerminal || showcaseAggressive) {
89
+ return {
90
+ maxTabs: showcaseAggressive ? 4 : 5,
91
+ maxCharsPerTab: showcaseAggressive ? 1e3 : isLongVideo ? 1800 : 2200,
92
+ maxLinesPerTab: showcaseAggressive ? 22 : 40,
93
+ maxJsonItemsPerTab: showcaseAggressive ? 6 : 12,
94
+ maxTotalChars: showcaseAggressive ? 3500 : 7e3
95
+ };
96
+ }
97
+ return {
98
+ maxTabs: 6,
99
+ maxCharsPerTab: 2800,
100
+ maxLinesPerTab: 60,
101
+ maxJsonItemsPerTab: 16,
102
+ maxTotalChars: 1e4
103
+ };
104
+ }
105
+ function optimizeTemplatePropsForRender(templateId, props, realVideoDuration) {
106
+ if (!Array.isArray(props.terminalTabs) || props.terminalTabs.length === 0) {
107
+ return { ...props };
108
+ }
109
+ const profile = getTemplateRenderOptimizationProfile(templateId, props, realVideoDuration);
110
+ const terminalTabs = props.terminalTabs.slice(0, profile.maxTabs).map((tab) => {
111
+ if (shouldPreserveTerminalTabContent(tab, profile)) {
112
+ return { ...tab };
113
+ }
114
+ const compactJson = compactTerminalJsonContent(tab, profile);
115
+ return {
116
+ ...tab,
117
+ content: compactJson || clampTextForTerminalRender(tab.content, profile)
118
+ };
119
+ });
120
+ let totalChars = terminalTabs.reduce((sum, tab) => sum + tab.content.length, 0);
121
+ while (totalChars > profile.maxTotalChars && terminalTabs.length > 1) {
122
+ terminalTabs.pop();
123
+ totalChars = terminalTabs.reduce((sum, tab) => sum + tab.content.length, 0);
124
+ }
125
+ if (totalChars > profile.maxTotalChars && terminalTabs.length === 1) {
126
+ const [tab] = terminalTabs;
127
+ const compactJson = compactTerminalJsonContent(tab, profile);
128
+ terminalTabs[0] = {
129
+ ...tab,
130
+ content: compactJson || clampTextForTerminalRender(tab.content, profile)
131
+ };
132
+ }
133
+ return {
134
+ ...props,
135
+ terminalTabs,
136
+ hasNetworkData: terminalTabs.length > 0
137
+ };
138
+ }
139
+ async function startRender(projectId, templateId, props, onProgress) {
140
+ const bundlePath = getBundlePath();
141
+ if (!bundlePath) {
142
+ throw new Error("Templates not installed. Bundle not found at ~/.discoverylab/templates/bundle/");
143
+ }
144
+ const template = getTemplate(templateId);
145
+ if (!template) {
146
+ throw new Error(`Template "${templateId}" not found in manifest`);
147
+ }
148
+ const jobId = generateJobId();
149
+ const outputDir = getOutputDir(projectId);
150
+ const outputPath = join(outputDir, `template-${templateId}.mp4`);
151
+ const job = {
152
+ id: jobId,
153
+ projectId,
154
+ templateId,
155
+ status: "queued",
156
+ progress: 0,
157
+ outputPath,
158
+ startedAt: Date.now()
159
+ };
160
+ renderJobs.set(jobId, job);
161
+ renderAsync(job, bundlePath, templateId, template.compositionId, props, onProgress).catch((err) => {
162
+ job.status = "error";
163
+ job.error = err.message;
164
+ job.completedAt = Date.now();
165
+ });
166
+ return job;
167
+ }
168
+ async function renderAsync(job, bundlePath, templateId, compositionId, props, onProgress) {
169
+ job.status = "rendering";
170
+ const originalCwd = process.cwd();
171
+ try {
172
+ process.chdir(DATA_DIR);
173
+ const { selectComposition, renderMedia } = await import("@remotion/renderer");
174
+ const realVideoDuration = getVideoDuration(props.videoUrl);
175
+ const optimizedProps = optimizeTemplatePropsForRender(templateId, props, realVideoDuration);
176
+ if (realVideoDuration && realVideoDuration > 0) {
177
+ optimizedProps.videoDuration = realVideoDuration;
178
+ }
179
+ const composition = await selectComposition({
180
+ serveUrl: bundlePath,
181
+ id: compositionId,
182
+ inputProps: optimizedProps
183
+ });
184
+ const fps = composition.fps || 30;
185
+ let durationOverride;
186
+ if (realVideoDuration && realVideoDuration > 0) {
187
+ const videoFrames = Math.ceil(realVideoDuration * fps);
188
+ if (videoFrames > composition.durationInFrames) {
189
+ durationOverride = videoFrames;
190
+ }
191
+ }
192
+ await renderMedia({
193
+ composition: durationOverride ? { ...composition, durationInFrames: durationOverride } : composition,
194
+ serveUrl: bundlePath,
195
+ codec: "h264",
196
+ outputLocation: job.outputPath,
197
+ inputProps: optimizedProps,
198
+ onProgress: ({ progress }) => {
199
+ job.progress = progress;
200
+ onProgress?.(progress);
201
+ }
202
+ });
203
+ job.status = "done";
204
+ job.progress = 1;
205
+ job.completedAt = Date.now();
206
+ } catch (err) {
207
+ job.status = "error";
208
+ job.error = err.message;
209
+ job.completedAt = Date.now();
210
+ throw err;
211
+ } finally {
212
+ process.chdir(originalCwd);
213
+ }
214
+ }
215
+ function getVideoDuration(videoUrl) {
216
+ try {
217
+ let filePath = videoUrl;
218
+ if (videoUrl.startsWith("http")) {
219
+ const url = new URL(videoUrl);
220
+ const pathParam = url.searchParams.get("path");
221
+ if (pathParam) {
222
+ filePath = decodeURIComponent(pathParam);
223
+ } else {
224
+ return null;
225
+ }
226
+ }
227
+ if (!existsSync(filePath)) return null;
228
+ const output = execSync(
229
+ `ffprobe -v quiet -print_format json -show_format "${filePath}"`,
230
+ { encoding: "utf-8", timeout: 1e4 }
231
+ );
232
+ const data = JSON.parse(output);
233
+ const duration = parseFloat(data.format?.duration || "0");
234
+ return duration > 0 ? duration : null;
235
+ } catch {
236
+ return null;
237
+ }
238
+ }
239
+ function getRenderJob(jobId) {
240
+ return renderJobs.get(jobId) ?? null;
241
+ }
242
+ function getProjectRenderJobs(projectId) {
243
+ return Array.from(renderJobs.values()).filter((j) => j.projectId === projectId);
244
+ }
245
+ function getCachedRender(projectId, templateId) {
246
+ const outputPath = join(EXPORTS_DIR, projectId, `template-${templateId}.mp4`);
247
+ if (existsSync(outputPath)) return outputPath;
248
+ return null;
249
+ }
250
+ function cleanupRenderJobs(maxAge = 36e5) {
251
+ const now = Date.now();
252
+ for (const [id, job] of renderJobs) {
253
+ if (job.completedAt && now - job.completedAt > maxAge) {
254
+ renderJobs.delete(id);
255
+ }
256
+ }
257
+ }
258
+
259
+ export {
260
+ startRender,
261
+ getRenderJob,
262
+ getProjectRenderJobs,
263
+ getCachedRender,
264
+ cleanupRenderJobs
265
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  __export
3
- } from "./chunk-MLKGABMK.js";
3
+ } from "./chunk-4VNS5WPM.js";
4
4
 
5
5
  // src/db/index.ts
6
6
  import Database from "better-sqlite3";
@@ -58,6 +58,11 @@ var projects = sqliteTable("projects", {
58
58
  // AI-generated requirements from linked content
59
59
  taskTestMap: text("task_test_map"),
60
60
  // AI-generated test map from linked content
61
+ // Marketing / Export
62
+ marketingTitle: text("marketing_title"),
63
+ // Cleaned title without device names
64
+ marketingDescription: text("marketing_description"),
65
+ // AI-generated marketing-quality description
61
66
  // Status
62
67
  status: text("status").default("draft"),
63
68
  // 'draft' | 'analyzed' | 'exported' | 'archived'
@@ -232,6 +237,18 @@ function createTables(sqlite) {
232
237
  sqlite.exec(`ALTER TABLE projects ADD COLUMN ocr_confidence REAL`);
233
238
  } catch (e) {
234
239
  }
240
+ try {
241
+ sqlite.exec(`ALTER TABLE projects ADD COLUMN marketing_title TEXT`);
242
+ } catch (e) {
243
+ }
244
+ try {
245
+ sqlite.exec(`ALTER TABLE projects ADD COLUMN marketing_description TEXT`);
246
+ } catch (e) {
247
+ }
248
+ try {
249
+ sqlite.exec(`UPDATE projects SET marketing_title = name WHERE marketing_title IS NULL`);
250
+ } catch (e) {
251
+ }
235
252
  sqlite.exec(`
236
253
  CREATE TABLE IF NOT EXISTS project_exports (
237
254
  id TEXT PRIMARY KEY,