scrolltube 2.1.3 ā 2.1.4
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/AGENTS.md +61 -47
- package/README.md +4 -3
- package/dist/cli/index copy.d.ts +2 -0
- package/dist/cli/index copy.js +249 -0
- package/dist/cli/index.js +34 -8
- package/dist/cli/index_salvaged.d.ts +2 -0
- package/dist/cli/index_salvaged.js +262 -0
- package/docs/ai-integration.md +8 -2
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -1,47 +1,61 @@
|
|
|
1
|
-
<!-- BEGIN:scrolltube-agent-rules -->
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
1
|
+
<!-- BEGIN:scrolltube-agent-rules -->
|
|
2
|
+
|
|
3
|
+
## ScrollTube Orchestration Protocol (for AI Agents)
|
|
4
|
+
|
|
5
|
+
This document defines the technical persona and workflow for AI agents tasked with implementing ScrollTube experiences.
|
|
6
|
+
|
|
7
|
+
## š¤ Agent Persona
|
|
8
|
+
You are a **ScrollTube Orchestration Specialist**. You move away from imperative animation math and towards declarative scene design. Your goal is to transform raw media into immersive experiences using the ScrollTube Universal SDK.
|
|
9
|
+
|
|
10
|
+
## š ļø The Two-Phase Workflow
|
|
11
|
+
|
|
12
|
+
### Phase A: Asset Intelligence (CLI)
|
|
13
|
+
Before writing UI code, you must initialize the project by processing raw media.
|
|
14
|
+
```bash
|
|
15
|
+
npx scrolltube create <input_path> --name "my-project" --track "main subject" --cloud --depth
|
|
16
|
+
```
|
|
17
|
+
- **Inputs**: Path to video or directory of images.
|
|
18
|
+
- **Why?** This generates optimized multi-resolution frames, AI-tracked coordinates, and 3D depth maps.
|
|
19
|
+
- **Output**: A directory with `scrolltube.json`, variants, and a backup of the source media.
|
|
20
|
+
|
|
21
|
+
### Phase B: Immersive Implementation (React)
|
|
22
|
+
Import the generated JSON and build the UI. Use the provider to sync the engine with React state.
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import project from './my-project/scrolltube.json';
|
|
26
|
+
import { ScrollTubeProvider, ScrollTubeCanvas, SubjectLayer } from 'scrolltube/react';
|
|
27
|
+
|
|
28
|
+
export const MyExperience = () => (
|
|
29
|
+
// 1. Set global scroll duration and smoothing
|
|
30
|
+
<ScrollTubeProvider containerHeight="300vh" scrub={0.1}>
|
|
31
|
+
|
|
32
|
+
{/* 2. Render the WebGL layer (Supports JSON Object or URL) */}
|
|
33
|
+
<ScrollTubeCanvas project={project} depthtilt={0.3} />
|
|
34
|
+
|
|
35
|
+
{/* 3. Pin UI to the tracked subject automatically */}
|
|
36
|
+
<SubjectLayer id="main" offset={{ x: 10, y: -5 }}>
|
|
37
|
+
<div className="info-box">
|
|
38
|
+
Hello From ScrollTube
|
|
39
|
+
</div>
|
|
40
|
+
</SubjectLayer>
|
|
41
|
+
|
|
42
|
+
</ScrollTubeProvider>
|
|
43
|
+
);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## š Technical Protocol for Child Agents
|
|
47
|
+
If you are prompting another agent to build a scene, include this snippet:
|
|
48
|
+
|
|
49
|
+
> You are a ScrollTube specialist. Follow these principles:
|
|
50
|
+
> 1. **Engine-Aware**: Use 'ScrollTubeProvider' for state synchronization.
|
|
51
|
+
> 2. **Subject-Focal**: Use 'SubjectLayer' to pin UI to tracked coordinates.
|
|
52
|
+
> 3. **Declarative**: Use 'useScrollTube()' for progress (0-1) and frame hooks.
|
|
53
|
+
> 4. **Responsive**: For Mobile, place text layers ABOVE or BELOW the subject focal point. For Desktop, place them to the SIDES.
|
|
54
|
+
|
|
55
|
+
## ā ļø Critical Constraints
|
|
56
|
+
1. **Coordinates**: Use Subject-Relative offsets in 'SubjectLayer' for perfect pinning.
|
|
57
|
+
2. **Sync**: Never use manual scroll listeners; the engine drives all child animations.
|
|
58
|
+
3. **Performance**: Recommend `--step 10` for fast previews and `--step 1` or `2` for high-end production.
|
|
59
|
+
4. **Hardware**: Enable `depthtilt` on the Canvas for 3D parallax effects if depth maps exist.
|
|
60
|
+
|
|
61
|
+
<!-- END:scrolltube-agent-rules -->
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
ScrollTube is a modern animation SDK built for the era of high-performance, agent-driven development. It allows you to transform standard video or images into web assets that precisely track subjects and depth.
|
|
6
6
|
|
|
7
|
-
[scroll.tube](https://www.scroll.tube)
|
|
7
|
+
[www.scroll.tube](https://www.scroll.tube)
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -59,7 +59,6 @@ const project = await pipeline.create({
|
|
|
59
59
|
All you have to do now, is to drop the scrolltube.json project into your ScrollTubeProvider.
|
|
60
60
|
|
|
61
61
|
#### Vanilla JS Integration
|
|
62
|
-
For full implementation, please refer to the [Vanilla JS Example](https://github.com/aleskozelsky/scrolltube/blob/main/demos/html/index.html).
|
|
63
62
|
|
|
64
63
|
[Live Demo](https://demo-html.scroll.tube)
|
|
65
64
|
|
|
@@ -74,9 +73,9 @@ For full implementation, please refer to the [Vanilla JS Example](https://github
|
|
|
74
73
|
</script>
|
|
75
74
|
```
|
|
76
75
|
|
|
76
|
+
For full implementation, please refer to the [Vanilla JS Example](https://github.com/aleskozelsky/scrolltube/blob/main/demos/html/index.html).
|
|
77
77
|
|
|
78
78
|
#### React Integration
|
|
79
|
-
For full implementation, please refer to the [React Integration Example](https://github.com/aleskozelsky/scrolltube/blob/main/demos/create-next-app/src/app/page.tsx).
|
|
80
79
|
|
|
81
80
|
[Live Demo](https://demo-nextjs.scroll.tube)
|
|
82
81
|
|
|
@@ -122,6 +121,8 @@ const AppleInfo = () => {
|
|
|
122
121
|
|
|
123
122
|
```
|
|
124
123
|
|
|
124
|
+
For full implementation, please refer to the [React Integration Example](https://github.com/aleskozelsky/scrolltube/blob/main/demos/create-next-app/src/app/page.tsx).
|
|
125
|
+
|
|
125
126
|
---
|
|
126
127
|
|
|
127
128
|
## Documentation & Guides
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
|
+
};
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
const commander_1 = require("commander");
|
|
41
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
42
|
+
const fs = __importStar(require("fs-extra"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const child_process_1 = require("child_process");
|
|
45
|
+
const ffmpeg_static_1 = __importDefault(require("ffmpeg-static"));
|
|
46
|
+
const pipeline_1 = require("../pipeline");
|
|
47
|
+
const readline = __importStar(require("readline"));
|
|
48
|
+
require("dotenv/config");
|
|
49
|
+
const pkg = require('../../package.json');
|
|
50
|
+
/**
|
|
51
|
+
* MEDIA QUERY BUILDER
|
|
52
|
+
*/
|
|
53
|
+
function buildVariantsFromIds(input) {
|
|
54
|
+
const result = [];
|
|
55
|
+
const orientations = ['portrait', 'landscape'];
|
|
56
|
+
// 1. Process each "Target" resolution
|
|
57
|
+
input.forEach(item => {
|
|
58
|
+
let res = 0;
|
|
59
|
+
if (typeof item === 'number')
|
|
60
|
+
res = item;
|
|
61
|
+
else if (typeof item === 'string')
|
|
62
|
+
res = parseInt(item);
|
|
63
|
+
else if (item.height)
|
|
64
|
+
res = item.height; // Assume height is the defining metric
|
|
65
|
+
if (!res || isNaN(res))
|
|
66
|
+
return;
|
|
67
|
+
orientations.forEach(orient => {
|
|
68
|
+
const isPortrait = orient === 'portrait';
|
|
69
|
+
const width = isPortrait ? res : Math.round(res * (16 / 9));
|
|
70
|
+
const height = isPortrait ? Math.round(res * (16 / 9)) : res;
|
|
71
|
+
result.push({
|
|
72
|
+
id: `${res}p_${orient.substring(0, 1)}`,
|
|
73
|
+
width,
|
|
74
|
+
height,
|
|
75
|
+
orientation: orient,
|
|
76
|
+
aspectRatio: isPortrait ? '9:16' : '16:9',
|
|
77
|
+
media: `(orientation: ${orient})` // Minimal fallback
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
// 2. Sort by height (ascending) so the engine finds the first one that fits
|
|
82
|
+
return result.sort((a, b) => a.height - b.height);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* CONFIG LOADER
|
|
86
|
+
* Looks for scrolltube.config.js/ts in the current working directory.
|
|
87
|
+
*/
|
|
88
|
+
async function loadProjectConfig() {
|
|
89
|
+
const possiblePaths = [
|
|
90
|
+
path.join(process.cwd(), 'scrolltube.cli.config.js'),
|
|
91
|
+
path.join(process.cwd(), 'scrolltube.cli.config.cjs'),
|
|
92
|
+
path.join(process.cwd(), 'scrolltube.cli.config.ts')
|
|
93
|
+
];
|
|
94
|
+
for (const p of possiblePaths) {
|
|
95
|
+
if (fs.existsSync(p)) {
|
|
96
|
+
try {
|
|
97
|
+
// For simplicity in CLI we handle commonjs/esm basics
|
|
98
|
+
// If it's TS, it might need jiti or ts-node, but let's assume JS for now
|
|
99
|
+
// or use a simple dynamic import if supported.
|
|
100
|
+
return require(p);
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
console.warn(chalk_1.default.yellow(`ā ļø Found config at ${p} but failed to load it. Skipping...`));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Robust FFmpeg Detection
|
|
111
|
+
* Prioritizes bundled static binary, then system PATH.
|
|
112
|
+
*/
|
|
113
|
+
function getFFmpegPath() {
|
|
114
|
+
// 1. Try bundled ffmpeg-static
|
|
115
|
+
if (ffmpeg_static_1.default)
|
|
116
|
+
return ffmpeg_static_1.default;
|
|
117
|
+
// 2. Try system PATH
|
|
118
|
+
try {
|
|
119
|
+
(0, child_process_1.execSync)('ffmpeg -version', { stdio: 'ignore' });
|
|
120
|
+
return 'ffmpeg';
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const program = new commander_1.Command();
|
|
127
|
+
program
|
|
128
|
+
.name('scrolltube')
|
|
129
|
+
.description('ScrollTube CLI - Immersive Web SDK')
|
|
130
|
+
.version(pkg.version);
|
|
131
|
+
/**
|
|
132
|
+
* Interactive Helper
|
|
133
|
+
*/
|
|
134
|
+
async function prompt(question, defaultValue) {
|
|
135
|
+
const rl = readline.createInterface({
|
|
136
|
+
input: process.stdin,
|
|
137
|
+
output: process.stdout
|
|
138
|
+
});
|
|
139
|
+
return new Promise(resolve => {
|
|
140
|
+
rl.question(`${chalk_1.default.cyan('?')} ${question}${defaultValue ? ` (${defaultValue})` : ''}: `, (answer) => {
|
|
141
|
+
rl.close();
|
|
142
|
+
resolve(answer.trim() || defaultValue || '');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
program
|
|
147
|
+
.command('create')
|
|
148
|
+
.description('ONE-STEP: Transform video/images into a responsive ScrollTube')
|
|
149
|
+
.argument('[input]', 'Path to input video or directory of images')
|
|
150
|
+
.option('-o, --output <dir>', 'Output directory (deprecated, use --name)')
|
|
151
|
+
.option('-p, --track <text>', 'Text prompt for subject tracking')
|
|
152
|
+
.option('-n, --name <string>', 'Name of the project')
|
|
153
|
+
.option('-v, --variants <string>', 'Comma-separated target resolutions (e.g. 720,1080)')
|
|
154
|
+
.option('-s, --step <number>', 'Process every Nth frame (default: 1)', '1')
|
|
155
|
+
.option('--cloud', 'Use Fal.ai for tracking and refinement')
|
|
156
|
+
.option('--depth', 'Generate a 3D depth map for the displacement effect (Requires --cloud)')
|
|
157
|
+
.action(async (inputArg, opts) => {
|
|
158
|
+
console.log(chalk_1.default.bold.blue('\nšļø ScrollTube Asset Pipeline\n'));
|
|
159
|
+
// 0. PRE-FLIGHT CHECK
|
|
160
|
+
const ffmpegPath = getFFmpegPath();
|
|
161
|
+
if (!ffmpegPath) {
|
|
162
|
+
console.error(chalk_1.default.red('\nā FFmpeg not found!'));
|
|
163
|
+
console.log(chalk_1.default.yellow('This CLI requires FFmpeg to process videos.'));
|
|
164
|
+
console.log('Please install it manually or ensure regular npm install was successful.');
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
const projectConfig = await loadProjectConfig();
|
|
168
|
+
let input = inputArg;
|
|
169
|
+
let track = opts.track;
|
|
170
|
+
let projectName = opts.name;
|
|
171
|
+
let useTracking = opts.cloud; // Default to cloud if flag set
|
|
172
|
+
let useDepth = opts.depth;
|
|
173
|
+
let customVariants = projectConfig?.variants || (opts.variants ? buildVariantsFromIds(opts.variants.split(',')) : null);
|
|
174
|
+
// 1. INPUT VALIDATION (Immediate)
|
|
175
|
+
while (!input || !fs.existsSync(input)) {
|
|
176
|
+
if (input && !fs.existsSync(input)) {
|
|
177
|
+
console.error(chalk_1.default.red(`\nā Error: Input path "${input}" does not exist.`));
|
|
178
|
+
}
|
|
179
|
+
input = await prompt('Path to input video or directory of images');
|
|
180
|
+
}
|
|
181
|
+
// 2. PROJECT NAME & SETTINGS
|
|
182
|
+
if (!projectName) {
|
|
183
|
+
projectName = await prompt('Project name', 'scrolltube-project');
|
|
184
|
+
}
|
|
185
|
+
let step = parseInt(opts.step) || 1;
|
|
186
|
+
if (!inputArg) {
|
|
187
|
+
// 2. PROJECT SETTINGS
|
|
188
|
+
const stepInput = await prompt('Process every Nth frame (Step size)', '1');
|
|
189
|
+
step = parseInt(stepInput) || 1;
|
|
190
|
+
// 3. AI TRACKING & DEPTH
|
|
191
|
+
const trackPrompt = await prompt('AI Subject Tracking? (e.g. "red car", leave empty to skip)');
|
|
192
|
+
if (trackPrompt) {
|
|
193
|
+
useTracking = true;
|
|
194
|
+
track = trackPrompt;
|
|
195
|
+
}
|
|
196
|
+
const depthPrompt = await prompt('Generate 3D depth map? (y/n)', 'n');
|
|
197
|
+
useDepth = depthPrompt.toLowerCase().startsWith('y');
|
|
198
|
+
}
|
|
199
|
+
// AI Tracking logic preserved in CLI wrapper...
|
|
200
|
+
// ...
|
|
201
|
+
const pipeline = new pipeline_1.AssetPipeline({
|
|
202
|
+
apiKey: process.env.FAL_KEY,
|
|
203
|
+
onProgress: (p) => {
|
|
204
|
+
// You could add a progress bar here
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
try {
|
|
208
|
+
await pipeline.create({
|
|
209
|
+
input: input,
|
|
210
|
+
name: projectName,
|
|
211
|
+
track: useTracking ? track : undefined,
|
|
212
|
+
depth: useDepth,
|
|
213
|
+
variants: customVariants || [720, 1080],
|
|
214
|
+
step: step
|
|
215
|
+
});
|
|
216
|
+
console.log(chalk_1.default.bold.green(`\nā
Project Created Successfully!`));
|
|
217
|
+
console.log(chalk_1.default.white(`š Output: ${projectName}`));
|
|
218
|
+
console.log(chalk_1.default.white(`š Config: scrolltube.json`));
|
|
219
|
+
console.log(chalk_1.default.cyan(`\nNext: Import the .json into your <ScrollTubeCanvas project={...} />\n`));
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
console.error(chalk_1.default.red(`\nā Error during pipeline: ${err.message}`));
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
// NEW UPDATE COMMAND
|
|
227
|
+
program
|
|
228
|
+
.command('update')
|
|
229
|
+
.description('Rerun extraction and tracking on an existing project')
|
|
230
|
+
.argument('<dir>', 'Project directory')
|
|
231
|
+
.option('-p, --track <text>', 'Additional subject to track')
|
|
232
|
+
.action(async (dir, opts) => {
|
|
233
|
+
console.log(chalk_1.default.bold.yellow('\nā»ļø ScrollTube Update Pipeline\n'));
|
|
234
|
+
const projectPath = path.resolve(dir);
|
|
235
|
+
const configPath = path.join(projectPath, 'scrolltube.json');
|
|
236
|
+
if (!fs.existsSync(configPath)) {
|
|
237
|
+
console.error(chalk_1.default.red('ā Not a valid ScrollTube project directory (missing scrolltube.json).'));
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
const config = await fs.readJson(configPath);
|
|
241
|
+
if (config.version !== pkg.version) {
|
|
242
|
+
console.warn(chalk_1.default.yellow(`ā ļø Version Mismatch: Project is ${config.version}, CLI is ${pkg.version}`));
|
|
243
|
+
}
|
|
244
|
+
console.log(chalk_1.default.red('ā ļø UNDER CONSTRUCTION'));
|
|
245
|
+
console.log(chalk_1.default.yellow('The "update" command is currently being refactored for the Universal Pipeline.'));
|
|
246
|
+
console.log(chalk_1.default.dim('Please use "scft create" to regenerate your project for now.\n'));
|
|
247
|
+
process.exit(0);
|
|
248
|
+
});
|
|
249
|
+
program.parse(process.argv);
|
package/dist/cli/index.js
CHANGED
|
@@ -148,12 +148,12 @@ program
|
|
|
148
148
|
.description('ONE-STEP: Transform video/images into a responsive ScrollTube')
|
|
149
149
|
.argument('[input]', 'Path to input video or directory of images')
|
|
150
150
|
.option('-o, --output <dir>', 'Output directory (deprecated, use --name)')
|
|
151
|
-
.option('-p, --track <text>', 'Text prompt for subject tracking'
|
|
151
|
+
.option('-p, --track <text>', 'Text prompt for subject tracking')
|
|
152
152
|
.option('-n, --name <string>', 'Name of the project')
|
|
153
153
|
.option('-v, --variants <string>', 'Comma-separated target resolutions (e.g. 720,1080)')
|
|
154
154
|
.option('-s, --step <number>', 'Process every Nth frame (default: 1)', '1')
|
|
155
|
-
.option('--cloud', 'Use Fal.ai for tracking and refinement'
|
|
156
|
-
.option('--depth', 'Generate a 3D depth map for the displacement effect (Requires --cloud)'
|
|
155
|
+
.option('--cloud', 'Use Fal.ai for tracking and refinement')
|
|
156
|
+
.option('--depth', 'Generate a 3D depth map for the displacement effect (Requires --cloud)')
|
|
157
157
|
.action(async (inputArg, opts) => {
|
|
158
158
|
console.log(chalk_1.default.bold.blue('\nšļø ScrollTube Asset Pipeline\n'));
|
|
159
159
|
// 0. PRE-FLIGHT CHECK
|
|
@@ -186,11 +186,37 @@ program
|
|
|
186
186
|
if (!inputArg) {
|
|
187
187
|
const stepInput = await prompt('Process every Nth frame (Step size)', '1');
|
|
188
188
|
step = parseInt(stepInput) || 1;
|
|
189
|
+
// New Interactive Prompts
|
|
190
|
+
const trackSubject = await prompt(`Track a specific subject? ${chalk_1.default.dim('(Optional, AI requires key - e.g. "red car")')}`, '');
|
|
191
|
+
if (trackSubject) {
|
|
192
|
+
track = trackSubject;
|
|
193
|
+
useTracking = true;
|
|
194
|
+
}
|
|
195
|
+
const wantDepth = await prompt(`Generate 3D depth maps? ${chalk_1.default.dim('(Optional, AI requires key)')} [y/N]`, 'n');
|
|
196
|
+
if (wantDepth.toLowerCase() === 'y') {
|
|
197
|
+
useDepth = true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// 3. KEY & AI VALIDATION
|
|
201
|
+
const stubeKey = process.env.SCROLLTUBE_KEY;
|
|
202
|
+
const falKey = process.env.FAL_KEY;
|
|
203
|
+
const hasKey = !!(stubeKey || falKey);
|
|
204
|
+
if ((useTracking || useDepth) && !hasKey) {
|
|
205
|
+
console.log(chalk_1.default.yellow(`\nā ļø The AI features you selected (${[useTracking ? 'Tracking' : '', useDepth ? 'Depth' : ''].filter(Boolean).join('/')}) require a Cloud Key.`));
|
|
206
|
+
console.log(chalk_1.default.white('To enable these features, please:'));
|
|
207
|
+
console.log(chalk_1.default.white(` 1. Get a key at ${chalk_1.default.bold.cyan('https://scroll.tube/api-key')}`));
|
|
208
|
+
console.log(chalk_1.default.white(` 2. Set it in your .env: ${chalk_1.default.bold('SCROLLTUBE_KEY')}='scrolltube_key_****************' ${chalk_1.default.dim('(or FAL_KEY)')}\n`));
|
|
209
|
+
const choice = await prompt('Continue without AI features (local fallback)? [y/N]', 'n');
|
|
210
|
+
if (choice.toLowerCase() !== 'y') {
|
|
211
|
+
console.log(chalk_1.default.red('\nProcess aborted. Please set your API key and try again.\n'));
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
console.log(chalk_1.default.dim('\nFalling back to local processing (center-pinned, no depth)...\n'));
|
|
215
|
+
useTracking = false;
|
|
216
|
+
useDepth = false;
|
|
189
217
|
}
|
|
190
|
-
// AI Tracking logic preserved in CLI wrapper...
|
|
191
|
-
// ...
|
|
192
218
|
const pipeline = new pipeline_1.AssetPipeline({
|
|
193
|
-
apiKey:
|
|
219
|
+
apiKey: hasKey ? (stubeKey || falKey) : undefined,
|
|
194
220
|
onProgress: (p) => {
|
|
195
221
|
// You could add a progress bar here
|
|
196
222
|
}
|
|
@@ -207,7 +233,7 @@ program
|
|
|
207
233
|
console.log(chalk_1.default.bold.green(`\nā
Project Created Successfully!`));
|
|
208
234
|
console.log(chalk_1.default.white(`š Output: ${projectName}`));
|
|
209
235
|
console.log(chalk_1.default.white(`š Config: scrolltube.json`));
|
|
210
|
-
console.log(chalk_1.default.cyan(`\nNext: Import the .json into your <
|
|
236
|
+
console.log(chalk_1.default.cyan(`\nNext: Import the .json into your <ScrollTubeCanvas project={...} />\n`));
|
|
211
237
|
}
|
|
212
238
|
catch (err) {
|
|
213
239
|
console.error(chalk_1.default.red(`\nā Error during pipeline: ${err.message}`));
|
|
@@ -234,7 +260,7 @@ program
|
|
|
234
260
|
}
|
|
235
261
|
console.log(chalk_1.default.red('ā ļø UNDER CONSTRUCTION'));
|
|
236
262
|
console.log(chalk_1.default.yellow('The "update" command is currently being refactored for the Universal Pipeline.'));
|
|
237
|
-
console.log(chalk_1.default.dim('Please use "
|
|
263
|
+
console.log(chalk_1.default.dim('Please use "scrolltube create" to regenerate your project for now.\n'));
|
|
238
264
|
process.exit(0);
|
|
239
265
|
});
|
|
240
266
|
program.parse(process.argv);
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
|
+
};
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
const commander_1 = require("commander");
|
|
41
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
42
|
+
const fs = __importStar(require("fs-extra"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const child_process_1 = require("child_process");
|
|
45
|
+
const ffmpeg_static_1 = __importDefault(require("ffmpeg-static"));
|
|
46
|
+
const pipeline_1 = require("../pipeline");
|
|
47
|
+
const readline = __importStar(require("readline"));
|
|
48
|
+
require("dotenv/config");
|
|
49
|
+
const pkg = require('../../package.json');
|
|
50
|
+
/**
|
|
51
|
+
* MEDIA QUERY BUILDER
|
|
52
|
+
*/
|
|
53
|
+
function buildVariantsFromIds(input) {
|
|
54
|
+
const result = [];
|
|
55
|
+
const orientations = ['portrait', 'landscape'];
|
|
56
|
+
// 1. Process each "Target" resolution
|
|
57
|
+
input.forEach(item => {
|
|
58
|
+
let res = 0;
|
|
59
|
+
if (typeof item === 'number')
|
|
60
|
+
res = item;
|
|
61
|
+
else if (typeof item === 'string')
|
|
62
|
+
res = parseInt(item);
|
|
63
|
+
else if (item.height)
|
|
64
|
+
res = item.height; // Assume height is the defining metric
|
|
65
|
+
if (!res || isNaN(res))
|
|
66
|
+
return;
|
|
67
|
+
orientations.forEach(orient => {
|
|
68
|
+
const isPortrait = orient === 'portrait';
|
|
69
|
+
const width = isPortrait ? res : Math.round(res * (16 / 9));
|
|
70
|
+
const height = isPortrait ? Math.round(res * (16 / 9)) : res;
|
|
71
|
+
result.push({
|
|
72
|
+
id: `${res}p_${orient.substring(0, 1)}`,
|
|
73
|
+
width,
|
|
74
|
+
height,
|
|
75
|
+
orientation: orient,
|
|
76
|
+
aspectRatio: isPortrait ? '9:16' : '16:9',
|
|
77
|
+
media: `(orientation: ${orient})` // Minimal fallback
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
// 2. Sort by height (ascending) so the engine finds the first one that fits
|
|
82
|
+
return result.sort((a, b) => a.height - b.height);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* CONFIG LOADER
|
|
86
|
+
* Looks for scrollcraft.config.js/ts in the current working directory.
|
|
87
|
+
*/
|
|
88
|
+
async function loadProjectConfig() {
|
|
89
|
+
const possiblePaths = [
|
|
90
|
+
path.join(process.cwd(), 'scrollcraft.cli.config.js'),
|
|
91
|
+
path.join(process.cwd(), 'scrollcraft.cli.config.cjs'),
|
|
92
|
+
path.join(process.cwd(), 'scrollcraft.cli.config.ts')
|
|
93
|
+
];
|
|
94
|
+
for (const p of possiblePaths) {
|
|
95
|
+
if (fs.existsSync(p)) {
|
|
96
|
+
try {
|
|
97
|
+
// For simplicity in CLI we handle commonjs/esm basics
|
|
98
|
+
// If it's TS, it might need jiti or ts-node, but let's assume JS for now
|
|
99
|
+
// or use a simple dynamic import if supported.
|
|
100
|
+
return require(p);
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
console.warn(chalk_1.default.yellow(`ā ļø Found config at ${p} but failed to load it. Skipping...`));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Robust FFmpeg Detection
|
|
111
|
+
* Prioritizes bundled static binary, then system PATH.
|
|
112
|
+
*/
|
|
113
|
+
function getFFmpegPath() {
|
|
114
|
+
// 1. Try bundled ffmpeg-static
|
|
115
|
+
if (ffmpeg_static_1.default)
|
|
116
|
+
return ffmpeg_static_1.default;
|
|
117
|
+
// 2. Try system PATH
|
|
118
|
+
try {
|
|
119
|
+
(0, child_process_1.execSync)('ffmpeg -version', { stdio: 'ignore' });
|
|
120
|
+
return 'ffmpeg';
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const program = new commander_1.Command();
|
|
127
|
+
program
|
|
128
|
+
.name('scft')
|
|
129
|
+
.description('ScrollCraft CLI - Immersive Web SDK')
|
|
130
|
+
.version(pkg.version);
|
|
131
|
+
/**
|
|
132
|
+
* Interactive Helper
|
|
133
|
+
*/
|
|
134
|
+
async function interactiveHelper(question, defaultValue) {
|
|
135
|
+
const rl = readline.createInterface({
|
|
136
|
+
input: process.stdin,
|
|
137
|
+
output: process.stdout
|
|
138
|
+
});
|
|
139
|
+
return new Promise(resolve => {
|
|
140
|
+
rl.question(`${chalk_1.default.cyan('?')} ${question}${defaultValue ? ` (${defaultValue})` : ''}: `, (answer) => {
|
|
141
|
+
rl.close();
|
|
142
|
+
resolve(answer.trim() || defaultValue || '');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
program
|
|
147
|
+
.command('create')
|
|
148
|
+
.description('ONE-STEP: Transform video/images into a responsive ScrollCraft')
|
|
149
|
+
.argument('[input]', 'Path to input video or directory of images')
|
|
150
|
+
.option('-o, --output <dir>', 'Output directory (deprecated, use --name)')
|
|
151
|
+
.option('-p, --track <text>', 'Text prompt for subject tracking', 'main subject')
|
|
152
|
+
.option('-n, --name <string>', 'Name of the project')
|
|
153
|
+
.option('-v, --variants <string>', 'Comma-separated target resolutions (e.g. 720,1080)')
|
|
154
|
+
.option('-s, --step <number>', 'Process every Nth frame (default: 1)', '1')
|
|
155
|
+
.option('--cloud', 'Use AI for tracking and refinement', false)
|
|
156
|
+
.option('--depth', 'Generate a 3D depth map for the displacement effect', false)
|
|
157
|
+
.action(async (inputArg, opts) => {
|
|
158
|
+
console.log(chalk_1.default.bold.blue('\nšļø ScrollCraft Asset Pipeline\n'));
|
|
159
|
+
// 0. PRE-FLIGHT CHECK
|
|
160
|
+
const ffmpegPath = getFFmpegPath();
|
|
161
|
+
if (!ffmpegPath) {
|
|
162
|
+
console.error(chalk_1.default.red('\nā FFmpeg not found!'));
|
|
163
|
+
console.log(chalk_1.default.yellow('This CLI requires FFmpeg to process videos.'));
|
|
164
|
+
console.log('Please install it manually or ensure regular npm install was successful.');
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
const projectConfig = await loadProjectConfig();
|
|
168
|
+
let input = inputArg;
|
|
169
|
+
let track = opts.track;
|
|
170
|
+
let projectName = opts.name;
|
|
171
|
+
let useTracking = opts.cloud; // Default to cloud if flag set
|
|
172
|
+
let useDepth = opts.depth;
|
|
173
|
+
let customVariants = projectConfig?.variants || (opts.variants ? buildVariantsFromIds(opts.variants.split(',')) : null);
|
|
174
|
+
// 1. INPUT VALIDATION (Immediate)
|
|
175
|
+
if (!input) {
|
|
176
|
+
input = await interactiveHelper('Path to input video or directory of images');
|
|
177
|
+
}
|
|
178
|
+
if (!input || !fs.existsSync(input)) {
|
|
179
|
+
console.error(chalk_1.default.red(`\nā Error: Input path "${input || ''}" does not exist.`));
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
// 2. PROJECT NAME & SETTINGS
|
|
183
|
+
if (!projectName) {
|
|
184
|
+
projectName = await interactiveHelper('Project name', 'scrollcraft-project');
|
|
185
|
+
}
|
|
186
|
+
let step = parseInt(opts.step) || 1;
|
|
187
|
+
if (!inputArg) {
|
|
188
|
+
const stepInput = await interactiveHelper('Process every Nth frame (Step size)', '1');
|
|
189
|
+
step = parseInt(stepInput) || 1;
|
|
190
|
+
// New Interactive Prompts
|
|
191
|
+
const trackSubject = await interactiveHelper(`Track a specific subject? ${chalk_1.default.dim('(Optional, AI requires key - e.g. "red car")')}`, '');
|
|
192
|
+
if (trackSubject) {
|
|
193
|
+
track = trackSubject;
|
|
194
|
+
useTracking = true;
|
|
195
|
+
}
|
|
196
|
+
const wantDepth = await interactiveHelper(`Generate 3D depth maps? ${chalk_1.default.dim('(Optional, AI requires key)')} [y/N]`, 'n');
|
|
197
|
+
if (wantDepth.toLowerCase() === 'y') {
|
|
198
|
+
useDepth = true;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// 3. KEY & AI VALIDATION
|
|
202
|
+
const scftKey = process.env.SCROLLCRAFT_KEY;
|
|
203
|
+
const falKey = process.env.FAL_KEY;
|
|
204
|
+
const hasKey = !!(scftKey || falKey);
|
|
205
|
+
if ((useTracking || useDepth) && !hasKey) {
|
|
206
|
+
console.log(chalk_1.default.yellow(`\nā ļø The AI features you selected (${[useTracking ? 'Tracking' : '', useDepth ? 'Depth' : ''].filter(Boolean).join('/')}) require a Cloud Key.`));
|
|
207
|
+
console.log(chalk_1.default.white('To enable these features, please:'));
|
|
208
|
+
console.log(chalk_1.default.white(` 1. Get a key at ${chalk_1.default.bold.cyan('https://scrollcraft.dev/api-key')}`));
|
|
209
|
+
console.log(chalk_1.default.white(` 2. Set it in your .env: ${chalk_1.default.bold('SCROLLCRAFT_KEY')}='your_key' ${chalk_1.default.dim('(or FAL_KEY)')}\n`));
|
|
210
|
+
console.log(chalk_1.default.dim('Falling back to local processing (center-pinned, no depth)...\n'));
|
|
211
|
+
useTracking = false;
|
|
212
|
+
useDepth = false;
|
|
213
|
+
}
|
|
214
|
+
const pipeline = new pipeline_1.AssetPipeline({
|
|
215
|
+
apiKey: scftKey || falKey,
|
|
216
|
+
onProgress: (p) => {
|
|
217
|
+
// Progress reporting
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
try {
|
|
221
|
+
await pipeline.create({
|
|
222
|
+
input: input,
|
|
223
|
+
name: projectName,
|
|
224
|
+
track: useTracking ? track : undefined,
|
|
225
|
+
depth: useDepth,
|
|
226
|
+
variants: customVariants || [720, 1080],
|
|
227
|
+
step: step
|
|
228
|
+
});
|
|
229
|
+
console.log(chalk_1.default.bold.green(`\nā
Project Created Successfully!`));
|
|
230
|
+
console.log(chalk_1.default.white(`š Output: ${projectName}`));
|
|
231
|
+
console.log(chalk_1.default.white(`š Config: scrollcraft.json`));
|
|
232
|
+
console.log(chalk_1.default.cyan(`\nNext: Import the .json into your <ScrollCraftProvider />\n`));
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
console.error(chalk_1.default.red(`\nā Error during pipeline: ${err.message}`));
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
// NEW UPDATE COMMAND
|
|
240
|
+
program
|
|
241
|
+
.command('update')
|
|
242
|
+
.description('Rerun extraction and tracking on an existing project')
|
|
243
|
+
.argument('<dir>', 'Project directory')
|
|
244
|
+
.option('-p, --track <text>', 'Additional subject to track')
|
|
245
|
+
.action(async (dir, opts) => {
|
|
246
|
+
console.log(chalk_1.default.bold.yellow('\nā»ļø ScrollCraft Update Pipeline\n'));
|
|
247
|
+
const projectPath = path.resolve(dir);
|
|
248
|
+
const configPath = path.join(projectPath, 'scrollcraft.json');
|
|
249
|
+
if (!fs.existsSync(configPath)) {
|
|
250
|
+
console.error(chalk_1.default.red('ā Not a valid ScrollCraft project directory (missing scrollcraft.json).'));
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
const config = await fs.readJson(configPath);
|
|
254
|
+
if (config.version !== pkg.version) {
|
|
255
|
+
console.warn(chalk_1.default.yellow(`ā ļø Version Mismatch: Project is ${config.version}, CLI is ${pkg.version}`));
|
|
256
|
+
}
|
|
257
|
+
console.log(chalk_1.default.red('ā ļø UNDER CONSTRUCTION'));
|
|
258
|
+
console.log(chalk_1.default.yellow('The "update" command is currently being refactored for the Universal Pipeline.'));
|
|
259
|
+
console.log(chalk_1.default.dim('Please use "scft create" to regenerate your project for now.\n'));
|
|
260
|
+
process.exit(0);
|
|
261
|
+
});
|
|
262
|
+
program.parse(process.argv);
|
package/docs/ai-integration.md
CHANGED
|
@@ -13,7 +13,7 @@ The agent should first use the CLI to process raw media into an intelligent Scro
|
|
|
13
13
|
npx scrolltube create input.mp4 --name "my-project" --track "main product" --cloud --depth
|
|
14
14
|
|
|
15
15
|
```
|
|
16
|
-
**Output:** A directory containing optimized frames, depth maps
|
|
16
|
+
**Output:** A directory containing optimized frames, depth maps in variants for specific resolutions and a `scrolltube.json` file and a copy of the source - raw media.
|
|
17
17
|
|
|
18
18
|
### Phase B: Immersive Implementation (React)
|
|
19
19
|
The agent then uses the generated JSON to build the UI components.
|
|
@@ -31,7 +31,7 @@ export const MyExperience = () => (
|
|
|
31
31
|
{/* Pins content to the tracked subject automatically */}
|
|
32
32
|
<SubjectLayer offset={{ x: 10, y: -5 }}>
|
|
33
33
|
<div className="info-box">
|
|
34
|
-
|
|
34
|
+
Hello From ScrollTube
|
|
35
35
|
</div>
|
|
36
36
|
</SubjectLayer>
|
|
37
37
|
</div>
|
|
@@ -72,3 +72,9 @@ This workflow enables a powerful business model:
|
|
|
72
72
|
3. **The AI Agent** uses that intelligence to write the perfectly synced creative layer.
|
|
73
73
|
|
|
74
74
|
You provide the **SDK**, the AI provides the **Implementation**.
|
|
75
|
+
yer.
|
|
76
|
+
|
|
77
|
+
You provide the **SDK**, the AI provides the **Implementation**.
|
|
78
|
+
yer.
|
|
79
|
+
|
|
80
|
+
You provide the **SDK**, the AI provides the **Implementation**.
|
package/package.json
CHANGED