scrollcraft 2.0.5 → 2.0.7
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.
- package/README.md +36 -20
- package/dist/cli/fal-service.js +1 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +146 -72
- package/dist/cli/processor.d.ts +1 -0
- package/dist/cli/processor.js +13 -6
- package/dist/core/CoreEngine.d.ts +31 -13
- package/dist/core/index.d.ts +3 -0
- package/dist/core/scrollcraft.umd.min.js +1 -1
- package/dist/core/types.d.ts +6 -76
- package/dist/core/types.js +1 -1
- package/dist/pipeline/browser-driver.js +176 -0
- package/dist/pipeline/fal-service.js +101 -0
- package/dist/pipeline/index.js +223 -0
- package/dist/pipeline/node-driver.js +108 -0
- package/dist/pipeline/types.js +2 -0
- package/dist/react/ScrollCraftProvider.d.ts +9 -8
- package/dist/react/index.js +1 -1
- package/package.json +7 -4
package/dist/core/types.js
CHANGED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.BrowserDriver = void 0;
|
|
37
|
+
class BrowserDriver {
|
|
38
|
+
files = new Map();
|
|
39
|
+
constructor() {
|
|
40
|
+
console.log('🌐 BrowserDriver initialized');
|
|
41
|
+
}
|
|
42
|
+
async readFile(path) {
|
|
43
|
+
const data = this.files.get(path);
|
|
44
|
+
if (!data)
|
|
45
|
+
throw new Error(`File not found: ${path}`);
|
|
46
|
+
if (typeof data === 'string')
|
|
47
|
+
return new TextEncoder().encode(data);
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
async writeFile(path, data) {
|
|
51
|
+
this.files.set(path, data);
|
|
52
|
+
}
|
|
53
|
+
async mkdir(path) {
|
|
54
|
+
// Virtual folders - no-op for simple Map implementation
|
|
55
|
+
}
|
|
56
|
+
async exists(path) {
|
|
57
|
+
return this.files.has(path);
|
|
58
|
+
}
|
|
59
|
+
async readdir(dirPath) {
|
|
60
|
+
const results = [];
|
|
61
|
+
for (const key of this.files.keys()) {
|
|
62
|
+
if (key.startsWith(dirPath)) {
|
|
63
|
+
// Simple relative path extraction
|
|
64
|
+
const relative = key.replace(dirPath, '').replace(/^[\\\/]/, '');
|
|
65
|
+
if (relative && !relative.includes('/') && !relative.includes('\\')) {
|
|
66
|
+
results.push(relative);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return results;
|
|
71
|
+
}
|
|
72
|
+
async remove(path) {
|
|
73
|
+
this.files.delete(path);
|
|
74
|
+
}
|
|
75
|
+
join(...parts) {
|
|
76
|
+
return parts.join('/').replace(/\/+/g, '/');
|
|
77
|
+
}
|
|
78
|
+
resolve(...parts) {
|
|
79
|
+
return this.join(...parts);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* EXTRACT FRAMES (via ffmpeg.wasm)
|
|
83
|
+
* Note: Requires SharedArrayBuffer & specific Headers if using multithreading.
|
|
84
|
+
*/
|
|
85
|
+
async extractFrames(videoSource, outputDir, onProgress) {
|
|
86
|
+
try {
|
|
87
|
+
// Dynamic import to keep core bundle small
|
|
88
|
+
const { FFmpeg } = await Promise.resolve().then(() => __importStar(require('@ffmpeg/ffmpeg')));
|
|
89
|
+
const { fetchFile, toBlobURL } = await Promise.resolve().then(() => __importStar(require('@ffmpeg/util')));
|
|
90
|
+
const ffmpeg = new FFmpeg();
|
|
91
|
+
ffmpeg.on('progress', ({ progress }) => {
|
|
92
|
+
if (onProgress)
|
|
93
|
+
onProgress(Math.round(progress * 100));
|
|
94
|
+
});
|
|
95
|
+
// Load FFmpeg WASM
|
|
96
|
+
// You'll need to provide the correct URL for the core/worker files in your WP plugin
|
|
97
|
+
await ffmpeg.load({
|
|
98
|
+
coreURL: await toBlobURL(`https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm/ffmpeg-core.js`, 'text/javascript'),
|
|
99
|
+
wasmURL: await toBlobURL(`https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm/ffmpeg-core.wasm`, 'application/wasm'),
|
|
100
|
+
});
|
|
101
|
+
const inputName = 'input.mp4';
|
|
102
|
+
await ffmpeg.writeFile(inputName, await fetchFile(videoSource));
|
|
103
|
+
// Extract as PNGs/WebPs (WebP might be faster if supported in the WASM build)
|
|
104
|
+
await ffmpeg.exec(['-i', inputName, `${outputDir}/frame_%04d.png`]);
|
|
105
|
+
// Move files from FFmpeg VFS to our Map FS
|
|
106
|
+
const files = await ffmpeg.listDir(outputDir);
|
|
107
|
+
for (const file of files) {
|
|
108
|
+
if (file.name.startsWith('frame_')) {
|
|
109
|
+
const data = await ffmpeg.readFile(`${outputDir}/${file.name}`);
|
|
110
|
+
await this.writeFile(this.join(outputDir, file.name), data);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
await ffmpeg.terminate();
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
console.error('FFmpeg WASM Error:', err);
|
|
117
|
+
throw new Error('Failed to extract frames in browser. Did you enable SharedArrayBuffer headers?');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* PROCESS IMAGE (via Canvas API)
|
|
122
|
+
* High-performance resizing and cropping using the browser's hardware-accelerated Canvas.
|
|
123
|
+
*/
|
|
124
|
+
async processImage(input, config, options = {}) {
|
|
125
|
+
// 1. Load image into a bitmap
|
|
126
|
+
let blob;
|
|
127
|
+
if (typeof input === 'string') {
|
|
128
|
+
const data = await this.readFile(input);
|
|
129
|
+
blob = new Blob([data]);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
blob = new Blob([input]);
|
|
133
|
+
}
|
|
134
|
+
const img = await createImageBitmap(blob);
|
|
135
|
+
// 2. Setup Canvas
|
|
136
|
+
const canvas = new OffscreenCanvas(config.width, config.height);
|
|
137
|
+
const ctx = canvas.getContext('2d');
|
|
138
|
+
if (!ctx)
|
|
139
|
+
throw new Error('Could not get Canvas context');
|
|
140
|
+
// 3. Smart Crop Logic (simplified to cover/center)
|
|
141
|
+
const scale = Math.max(config.width / img.width, config.height / img.height);
|
|
142
|
+
const x = (config.width - img.width * scale) / 2;
|
|
143
|
+
const y = (config.height - img.height * scale) / 2;
|
|
144
|
+
// Apply filters
|
|
145
|
+
let filters = '';
|
|
146
|
+
if (options.grayscale)
|
|
147
|
+
filters += 'grayscale(100%) ';
|
|
148
|
+
if (options.blur)
|
|
149
|
+
filters += `blur(${options.blur}px) `;
|
|
150
|
+
if (filters)
|
|
151
|
+
ctx.filter = filters.trim();
|
|
152
|
+
ctx.drawImage(img, x, y, img.width * scale, img.height * scale);
|
|
153
|
+
// 4. Encode to WebP
|
|
154
|
+
const outputBlob = await canvas.convertToBlob({
|
|
155
|
+
type: 'image/webp',
|
|
156
|
+
quality: 0.8
|
|
157
|
+
});
|
|
158
|
+
return new Uint8Array(await outputBlob.arrayBuffer());
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* ZIP PROJECT (via JSZip)
|
|
162
|
+
* Bundles all processed assets into a single file for upload or download.
|
|
163
|
+
*/
|
|
164
|
+
async zipProject(outDir) {
|
|
165
|
+
const { default: JSZip } = await Promise.resolve().then(() => __importStar(require('jszip')));
|
|
166
|
+
const zip = new JSZip();
|
|
167
|
+
for (const [path, data] of this.files.entries()) {
|
|
168
|
+
if (path.startsWith(outDir)) {
|
|
169
|
+
const relativePath = path.replace(outDir, '').replace(/^[\\\/]/, '');
|
|
170
|
+
zip.file(relativePath, data);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return await zip.generateAsync({ type: 'uint8array' });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
exports.BrowserDriver = BrowserDriver;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FalService = void 0;
|
|
4
|
+
const client_1 = require("@fal-ai/client");
|
|
5
|
+
class FalService {
|
|
6
|
+
options;
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
// In node, we might fallback to process.env if not provided
|
|
10
|
+
const key = options.apiKey || (typeof process !== 'undefined' ? process.env?.FAL_KEY : '');
|
|
11
|
+
if (!key && !options.proxyUrl) {
|
|
12
|
+
// Don't throw yet, only when a cloud method is called
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
getAuthHeaders() {
|
|
16
|
+
return {}; // fal-ai/client handles FAL_KEY from env or we can set it
|
|
17
|
+
}
|
|
18
|
+
async trackSubject(input, driver, prompt = "main subject") {
|
|
19
|
+
let videoUrl;
|
|
20
|
+
if (typeof input === 'string') {
|
|
21
|
+
// Local path or URL
|
|
22
|
+
if (await driver.exists(input)) {
|
|
23
|
+
videoUrl = await this.uploadFile(input, driver);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
videoUrl = input;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
// File or Blob
|
|
31
|
+
videoUrl = await client_1.fal.storage.upload(input);
|
|
32
|
+
}
|
|
33
|
+
console.log(`🤖 AI is tracking "${prompt}" via SAM 3...`);
|
|
34
|
+
const result = await client_1.fal.subscribe("fal-ai/sam-3/video-rle", {
|
|
35
|
+
input: {
|
|
36
|
+
video_url: videoUrl,
|
|
37
|
+
prompt: prompt,
|
|
38
|
+
},
|
|
39
|
+
logs: true,
|
|
40
|
+
});
|
|
41
|
+
const payload = result.data || result;
|
|
42
|
+
const boxes = payload.boxes;
|
|
43
|
+
if (!boxes || !Array.isArray(boxes) || boxes.length === 0) {
|
|
44
|
+
throw new Error(`AI tracking returned no data.`);
|
|
45
|
+
}
|
|
46
|
+
return this.mapBoxesToTrackingData(boxes);
|
|
47
|
+
}
|
|
48
|
+
async generateDepthMap(input, driver) {
|
|
49
|
+
let videoUrl;
|
|
50
|
+
if (typeof input === 'string') {
|
|
51
|
+
if (await driver.exists(input)) {
|
|
52
|
+
videoUrl = await this.uploadFile(input, driver);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
videoUrl = input;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
videoUrl = await client_1.fal.storage.upload(input);
|
|
60
|
+
}
|
|
61
|
+
console.log(`🤖 AI is generating Depth Map...`);
|
|
62
|
+
const result = await client_1.fal.subscribe("fal-ai/video-depth-anything", {
|
|
63
|
+
input: {
|
|
64
|
+
video_url: videoUrl,
|
|
65
|
+
model_size: "VDA-Base",
|
|
66
|
+
},
|
|
67
|
+
logs: true
|
|
68
|
+
});
|
|
69
|
+
const payload = result.data || result;
|
|
70
|
+
if (!payload.video || !payload.video.url) {
|
|
71
|
+
throw new Error(`AI Depth Map generation failed.`);
|
|
72
|
+
}
|
|
73
|
+
return payload.video.url;
|
|
74
|
+
}
|
|
75
|
+
async uploadFile(filePath, driver) {
|
|
76
|
+
const data = await driver.readFile(filePath);
|
|
77
|
+
return await client_1.fal.storage.upload(new Blob([data]));
|
|
78
|
+
}
|
|
79
|
+
mapBoxesToTrackingData(boxes) {
|
|
80
|
+
let lastKnown = { x: 0.5, y: 0.5, scale: 0 };
|
|
81
|
+
return boxes.map((frameBoxes, i) => {
|
|
82
|
+
if (frameBoxes && Array.isArray(frameBoxes)) {
|
|
83
|
+
let box = null;
|
|
84
|
+
if (typeof frameBoxes[0] === 'number' && frameBoxes.length >= 4) {
|
|
85
|
+
box = frameBoxes;
|
|
86
|
+
}
|
|
87
|
+
else if (Array.isArray(frameBoxes[0]) && frameBoxes[0].length >= 4) {
|
|
88
|
+
box = frameBoxes[0];
|
|
89
|
+
}
|
|
90
|
+
else if (typeof frameBoxes[0] === 'object' && frameBoxes[0].box_2d) {
|
|
91
|
+
box = frameBoxes[0].box_2d;
|
|
92
|
+
}
|
|
93
|
+
if (box) {
|
|
94
|
+
lastKnown = { x: box[0], y: box[1], scale: box[2] * box[3] };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { frame: i, ...lastKnown };
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
exports.FalService = FalService;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.AssetPipeline = void 0;
|
|
37
|
+
const fal_service_1 = require("./fal-service");
|
|
38
|
+
class AssetPipeline {
|
|
39
|
+
driver;
|
|
40
|
+
options;
|
|
41
|
+
fal;
|
|
42
|
+
constructor(options = {}) {
|
|
43
|
+
this.options = options;
|
|
44
|
+
this.fal = new fal_service_1.FalService({ apiKey: options.apiKey, proxyUrl: options.proxyUrl });
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* INITIALIZE DRIVER
|
|
48
|
+
* Detects environment and loads the appropriate driver dynamically.
|
|
49
|
+
*/
|
|
50
|
+
async init() {
|
|
51
|
+
if (this.driver)
|
|
52
|
+
return;
|
|
53
|
+
if (typeof window !== 'undefined') {
|
|
54
|
+
// Browser Environment
|
|
55
|
+
try {
|
|
56
|
+
// @ts-ignore - Assuming BrowserDriver will exist in same folder
|
|
57
|
+
const { BrowserDriver } = await Promise.resolve().then(() => __importStar(require('./browser-driver')));
|
|
58
|
+
this.driver = new BrowserDriver();
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
throw new Error('Could not load BrowserDriver. Ensure @scrollcraft/pipeline/browser is available.');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// Node Environment
|
|
66
|
+
try {
|
|
67
|
+
const { NodeDriver } = await Promise.resolve().then(() => __importStar(require('./node-driver')));
|
|
68
|
+
this.driver = new NodeDriver();
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
throw new Error('Could not load NodeDriver. Ensure @scrollcraft/pipeline/node (sharp, ffmpeg) is installed.');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
report(step, percent, message) {
|
|
76
|
+
if (this.options.onProgress) {
|
|
77
|
+
this.options.onProgress({ step, percent, message });
|
|
78
|
+
}
|
|
79
|
+
console.log(`[${step}] ${percent}% - ${message}`);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* THE MAIN ORCHESTRATOR
|
|
83
|
+
*/
|
|
84
|
+
async create(opts) {
|
|
85
|
+
await this.init();
|
|
86
|
+
const { input, name, track, hasDepth, step = 1 } = opts;
|
|
87
|
+
const outDir = this.driver.resolve(name);
|
|
88
|
+
const tempDir = this.driver.join(outDir, '.temp-frames');
|
|
89
|
+
this.report('initializing', 0, `Creating project: ${name}`);
|
|
90
|
+
await this.driver.mkdir(outDir);
|
|
91
|
+
await this.driver.mkdir(tempDir);
|
|
92
|
+
// 1. FRAME EXTRACTION
|
|
93
|
+
this.report('extracting', 10, 'Extracting frames from source...');
|
|
94
|
+
await this.driver.extractFrames(input, tempDir);
|
|
95
|
+
// 2. AI TRACKING & DEPTH
|
|
96
|
+
let trackingData = [];
|
|
97
|
+
let isDepthActive = false;
|
|
98
|
+
if (track || hasDepth) {
|
|
99
|
+
this.report('tracking', 30, 'Performing AI analysis...');
|
|
100
|
+
if (track) {
|
|
101
|
+
trackingData = await this.fal.trackSubject(input, this.driver, track);
|
|
102
|
+
}
|
|
103
|
+
if (hasDepth) {
|
|
104
|
+
this.report('depth', 40, 'Generating depth maps...');
|
|
105
|
+
const depthVideoUrl = await this.fal.generateDepthMap(input, this.driver);
|
|
106
|
+
// Download and extract depth frames
|
|
107
|
+
const response = await fetch(depthVideoUrl);
|
|
108
|
+
const buffer = await response.arrayBuffer();
|
|
109
|
+
const depthPath = this.driver.join(tempDir, 'depth_video.mp4');
|
|
110
|
+
await this.driver.writeFile(depthPath, new Uint8Array(buffer));
|
|
111
|
+
await this.driver.extractFrames(depthPath, tempDir); // Note: needs to handle prefix
|
|
112
|
+
isDepthActive = true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Default tracking if none
|
|
116
|
+
if (trackingData.length === 0) {
|
|
117
|
+
const files = await this.driver.readdir(tempDir);
|
|
118
|
+
const frameFiles = files.filter(f => f.startsWith('frame_'));
|
|
119
|
+
trackingData = frameFiles.map((_, i) => ({ frame: i, x: 0.5, y: 0.5, scale: 0 }));
|
|
120
|
+
}
|
|
121
|
+
// 3. VARIANT GENERATION
|
|
122
|
+
this.report('processing', 60, 'Generating optimized variants...');
|
|
123
|
+
const variants = await this.processVariants(tempDir, trackingData, {
|
|
124
|
+
step,
|
|
125
|
+
hasDepth: isDepthActive,
|
|
126
|
+
variants: this.normalizeVariants(opts.variants),
|
|
127
|
+
outDir
|
|
128
|
+
});
|
|
129
|
+
// 4. SAVE CONFIG
|
|
130
|
+
this.report('saving', 90, 'Finalizing project configuration...');
|
|
131
|
+
const config = await this.saveConfig(variants, outDir);
|
|
132
|
+
// Cleanup
|
|
133
|
+
await this.driver.remove(tempDir);
|
|
134
|
+
this.report('saving', 100, 'Project ready!');
|
|
135
|
+
if (opts.outputZip && this.driver.zipProject) {
|
|
136
|
+
return await this.driver.zipProject(outDir);
|
|
137
|
+
}
|
|
138
|
+
return config;
|
|
139
|
+
}
|
|
140
|
+
normalizeVariants(v) {
|
|
141
|
+
if (v.length === 0)
|
|
142
|
+
return [];
|
|
143
|
+
if (typeof v[0] === 'object')
|
|
144
|
+
return v;
|
|
145
|
+
// Convert numbers to baseline portrait/landscape pairs
|
|
146
|
+
const res = v;
|
|
147
|
+
const normalized = [];
|
|
148
|
+
res.forEach(r => {
|
|
149
|
+
normalized.push({
|
|
150
|
+
id: `${r}p_p`, width: r, height: Math.round(r * (16 / 9)),
|
|
151
|
+
orientation: 'portrait', aspectRatio: '9:16', media: '(orientation: portrait)'
|
|
152
|
+
});
|
|
153
|
+
normalized.push({
|
|
154
|
+
id: `${r}p_l`, width: Math.round(r * (16 / 9)), height: r,
|
|
155
|
+
orientation: 'landscape', aspectRatio: '16:9', media: '(orientation: landscape)'
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
return normalized;
|
|
159
|
+
}
|
|
160
|
+
async processVariants(tempDir, trackingData, options) {
|
|
161
|
+
const { step, outDir } = options;
|
|
162
|
+
const allFiles = await this.driver.readdir(tempDir);
|
|
163
|
+
const allFrames = allFiles.filter(f => f.startsWith('frame_')).sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
|
164
|
+
const framesToProcess = allFrames.filter((_, i) => i % step === 0);
|
|
165
|
+
const assetVariants = [];
|
|
166
|
+
for (const config of options.variants) {
|
|
167
|
+
const variantDir = this.driver.join(outDir, config.id);
|
|
168
|
+
await this.driver.mkdir(variantDir);
|
|
169
|
+
const variantTracking = [];
|
|
170
|
+
for (let i = 0; i < framesToProcess.length; i++) {
|
|
171
|
+
const originalIndex = i * step;
|
|
172
|
+
const frameName = framesToProcess[i];
|
|
173
|
+
const framePath = this.driver.join(tempDir, frameName);
|
|
174
|
+
const targetPath = this.driver.join(variantDir, `index_${i}.webp`);
|
|
175
|
+
const subject = trackingData.find(f => f.frame === originalIndex) || { frame: originalIndex, x: 0.5, y: 0.5, scale: 0 };
|
|
176
|
+
const imageBuffer = await this.driver.processImage(framePath, config, {});
|
|
177
|
+
await this.driver.writeFile(targetPath, imageBuffer);
|
|
178
|
+
if (options.hasDepth) {
|
|
179
|
+
const numStr = frameName.match(/(\d+)/)?.[1] || "";
|
|
180
|
+
const depthFile = allFiles.find(f => f.startsWith('depth_') && f.includes(numStr));
|
|
181
|
+
if (depthFile) {
|
|
182
|
+
const depthPath = this.driver.join(tempDir, depthFile);
|
|
183
|
+
const depthBuffer = await this.driver.processImage(depthPath, config, { grayscale: true, blur: 2 });
|
|
184
|
+
await this.driver.writeFile(this.driver.join(variantDir, `index_${i}_depth.webp`), depthBuffer);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
variantTracking.push({ ...subject, frame: i });
|
|
188
|
+
}
|
|
189
|
+
await this.driver.writeFile(this.driver.join(variantDir, '000_tracking-main.json'), JSON.stringify(variantTracking, null, 2));
|
|
190
|
+
assetVariants.push({
|
|
191
|
+
id: config.id,
|
|
192
|
+
media: config.media,
|
|
193
|
+
width: config.width,
|
|
194
|
+
height: config.height,
|
|
195
|
+
orientation: config.orientation,
|
|
196
|
+
aspectRatio: config.aspectRatio,
|
|
197
|
+
path: `./${config.id}`,
|
|
198
|
+
frameCount: framesToProcess.length,
|
|
199
|
+
hasDepthMap: options.hasDepth,
|
|
200
|
+
subjects: ['main']
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return assetVariants;
|
|
204
|
+
}
|
|
205
|
+
async saveConfig(variants, outDir) {
|
|
206
|
+
const pkg = { version: '2.0.6' }; // In real app, import from package.json
|
|
207
|
+
const config = {
|
|
208
|
+
version: pkg.version,
|
|
209
|
+
settings: { baseResolution: { width: 1920, height: 1080 }, scrollMode: 'vh' },
|
|
210
|
+
assets: [{ id: "main-sequence", strategy: "adaptive", variants: variants }],
|
|
211
|
+
timeline: {
|
|
212
|
+
totalDuration: "300vh",
|
|
213
|
+
scenes: [{
|
|
214
|
+
id: "scene-1", assetId: "main-sequence", startProgress: 0, duration: 1,
|
|
215
|
+
assetRange: [0, variants[0].frameCount - 1], layers: []
|
|
216
|
+
}]
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
await this.driver.writeFile(this.driver.join(outDir, 'scrollcraft.json'), JSON.stringify(config, null, 2));
|
|
220
|
+
return config;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
exports.AssetPipeline = AssetPipeline;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.NodeDriver = void 0;
|
|
40
|
+
const fs = __importStar(require("fs-extra"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const sharp_1 = __importDefault(require("sharp"));
|
|
43
|
+
const child_process_1 = require("child_process");
|
|
44
|
+
const ffmpeg_static_1 = __importDefault(require("ffmpeg-static"));
|
|
45
|
+
class NodeDriver {
|
|
46
|
+
ffmpegPath;
|
|
47
|
+
constructor() {
|
|
48
|
+
this.ffmpegPath = ffmpeg_static_1.default || 'ffmpeg';
|
|
49
|
+
}
|
|
50
|
+
async readFile(filePath) {
|
|
51
|
+
return await fs.readFile(filePath);
|
|
52
|
+
}
|
|
53
|
+
async writeFile(filePath, data) {
|
|
54
|
+
if (typeof data === 'string') {
|
|
55
|
+
await fs.writeFile(filePath, data);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
await fs.writeFile(filePath, Buffer.from(data));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async mkdir(dirPath) {
|
|
62
|
+
await fs.ensureDir(dirPath);
|
|
63
|
+
}
|
|
64
|
+
async exists(filePath) {
|
|
65
|
+
return await fs.pathExists(filePath);
|
|
66
|
+
}
|
|
67
|
+
async readdir(dirPath) {
|
|
68
|
+
return await fs.readdir(dirPath);
|
|
69
|
+
}
|
|
70
|
+
async remove(filePath) {
|
|
71
|
+
await fs.remove(filePath);
|
|
72
|
+
}
|
|
73
|
+
join(...parts) {
|
|
74
|
+
return path.join(...parts);
|
|
75
|
+
}
|
|
76
|
+
resolve(...parts) {
|
|
77
|
+
return path.resolve(...parts);
|
|
78
|
+
}
|
|
79
|
+
async extractFrames(videoSource, outputDir, onProgress) {
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
// For simplicity, we use execSync in a promise or spawn for progress
|
|
82
|
+
try {
|
|
83
|
+
// ffmpeg -i input output%04d.png
|
|
84
|
+
// For now, let's keep it simple like existing CLI
|
|
85
|
+
(0, child_process_1.execSync)(`"${this.ffmpegPath}" -i "${videoSource}" "${outputDir}/frame_%04d.png"`, { stdio: 'inherit' });
|
|
86
|
+
resolve();
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
reject(err);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
async processImage(input, config, options = {}) {
|
|
94
|
+
let pipeline = (0, sharp_1.default)(input)
|
|
95
|
+
.resize(config.width, config.height, {
|
|
96
|
+
fit: 'cover',
|
|
97
|
+
position: 'center' // Placeholder for smart-crop logic if we move it here
|
|
98
|
+
});
|
|
99
|
+
if (options.grayscale) {
|
|
100
|
+
pipeline = pipeline.grayscale();
|
|
101
|
+
}
|
|
102
|
+
if (options.blur) {
|
|
103
|
+
pipeline = pipeline.blur(options.blur);
|
|
104
|
+
}
|
|
105
|
+
return await pipeline.webp({ quality: 80 }).toBuffer();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
exports.NodeDriver = NodeDriver;
|
|
@@ -4,21 +4,22 @@ import { ProjectConfiguration } from '../core/types';
|
|
|
4
4
|
interface SCFTContext {
|
|
5
5
|
progress: number;
|
|
6
6
|
frame: number;
|
|
7
|
-
subjectCoords: {
|
|
8
|
-
x: number;
|
|
9
|
-
y: number;
|
|
10
|
-
};
|
|
11
7
|
engine: CoreEngine | null;
|
|
12
8
|
}
|
|
13
|
-
export
|
|
14
|
-
project: ProjectConfiguration;
|
|
9
|
+
export interface ScrollCraftProviderProps {
|
|
10
|
+
project: ProjectConfiguration | string;
|
|
15
11
|
children: React.ReactNode;
|
|
16
|
-
|
|
12
|
+
containerHeight?: string;
|
|
13
|
+
canvasHeight?: string;
|
|
14
|
+
offset?: any;
|
|
15
|
+
scrub?: number;
|
|
16
|
+
}
|
|
17
|
+
export declare const ScrollCraftProvider: React.FC<ScrollCraftProviderProps>;
|
|
17
18
|
export declare const ScrollCraftCanvas: React.FC<{
|
|
18
|
-
assetId?: string;
|
|
19
19
|
style?: React.CSSProperties;
|
|
20
20
|
}>;
|
|
21
21
|
export declare const SubjectLayer: React.FC<{
|
|
22
|
+
id?: string;
|
|
22
23
|
offset?: {
|
|
23
24
|
x: number;
|
|
24
25
|
y: number;
|