@vorec/cli 0.10.0 → 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.
- package/README.md +2 -3
- package/dist/commands/run.d.ts +0 -1
- package/dist/commands/run.js +45 -58
- package/dist/index.js +3 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,9 +49,8 @@ Create a `vorec.json` file describing the actions to record:
|
|
|
49
49
|
| Command | What it does |
|
|
50
50
|
|---------|-------------|
|
|
51
51
|
| `vorec init` | Save your API key |
|
|
52
|
-
| `vorec run <manifest>` | Record and
|
|
53
|
-
| `vorec run <manifest> --
|
|
54
|
-
| `vorec run <manifest> --auto --skip-record` | Resume an existing project |
|
|
52
|
+
| `vorec run <manifest>` | Record, upload, and generate narrated tutorial |
|
|
53
|
+
| `vorec run <manifest> --skip-record --video <file>` | Use existing video instead of recording |
|
|
55
54
|
| `vorec upload <video>` | Upload a video file directly |
|
|
56
55
|
| `vorec status` | Check project status |
|
|
57
56
|
|
package/dist/commands/run.d.ts
CHANGED
package/dist/commands/run.js
CHANGED
|
@@ -3,27 +3,12 @@ import ora from 'ora';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { loadManifest } from '../manifest.js';
|
|
5
5
|
import { apiCall, uploadVideo } from '../api.js';
|
|
6
|
-
import { saveProjectState
|
|
6
|
+
import { saveProjectState } from '../config.js';
|
|
7
7
|
export async function runCommand(manifestPath, opts) {
|
|
8
8
|
const manifest = loadManifest(manifestPath);
|
|
9
9
|
console.log(chalk.bold(`Vorec: ${manifest.title}`));
|
|
10
10
|
console.log(`Actions: ${manifest.actions.length}, URL: ${manifest.url}\n`);
|
|
11
|
-
//
|
|
12
|
-
const savedState = loadProjectState();
|
|
13
|
-
const resuming = opts.skipRecord && savedState?.projectId && savedState?.status === 'analyzing';
|
|
14
|
-
if (resuming) {
|
|
15
|
-
// Resume: skip recording + upload, go straight to analysis
|
|
16
|
-
console.log(`Resuming project: ${savedState.projectId}`);
|
|
17
|
-
if (opts.auto) {
|
|
18
|
-
await runAnalysis(savedState.projectId, manifest);
|
|
19
|
-
}
|
|
20
|
-
else {
|
|
21
|
-
const status = await apiCall('get-status', { projectId: savedState.projectId });
|
|
22
|
-
console.log(`\n${chalk.bold('Editor:')} ${status.editor_url}`);
|
|
23
|
-
}
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
// Fresh run: record → upload → (optionally analyze)
|
|
11
|
+
// Record → upload → generate
|
|
27
12
|
let videoPath = opts.video;
|
|
28
13
|
let trackedActions = [];
|
|
29
14
|
let videoDuration = 0;
|
|
@@ -64,6 +49,15 @@ export async function runCommand(manifestPath, opts) {
|
|
|
64
49
|
const ext = videoPath.split('.').pop()?.toLowerCase() || 'mp4';
|
|
65
50
|
const mimeMap = { mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime', mkv: 'video/x-matroska' };
|
|
66
51
|
const contentType = mimeMap[ext] || 'video/mp4';
|
|
52
|
+
// Get real video duration from file (ffprobe)
|
|
53
|
+
if (!videoDuration) {
|
|
54
|
+
try {
|
|
55
|
+
const { execSync } = await import('node:child_process');
|
|
56
|
+
const probe = execSync(`ffprobe -v error -show_entries format=duration -of csv=p=0 "${videoPath}"`, { encoding: 'utf-8' });
|
|
57
|
+
videoDuration = parseFloat(probe.trim()) || 0;
|
|
58
|
+
}
|
|
59
|
+
catch { /* ffprobe not available or failed — duration stays 0 */ }
|
|
60
|
+
}
|
|
67
61
|
// Step 2: Create project + upload
|
|
68
62
|
const spinner = ora('Creating project...').start();
|
|
69
63
|
const createResult = await apiCall('create-project', {
|
|
@@ -87,7 +81,7 @@ export async function runCommand(manifestPath, opts) {
|
|
|
87
81
|
await apiCall('confirm-upload', {
|
|
88
82
|
projectId: createResult.projectId,
|
|
89
83
|
storage_key: createResult.storage_key,
|
|
90
|
-
video_duration: videoDuration
|
|
84
|
+
video_duration: videoDuration > 0 ? videoDuration : undefined,
|
|
91
85
|
video_width: videoWidth,
|
|
92
86
|
video_height: videoHeight,
|
|
93
87
|
});
|
|
@@ -98,57 +92,50 @@ export async function runCommand(manifestPath, opts) {
|
|
|
98
92
|
status: 'analyzing',
|
|
99
93
|
});
|
|
100
94
|
spinner.succeed(`Project created: ${createResult.projectId}`);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
console.log(`\nVideo uploaded. Run with --auto to continue processing.`);
|
|
104
|
-
console.log(`${chalk.bold('Editor:')} ${status.editor_url}`);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
// Auto mode: continue with analysis
|
|
108
|
-
await runAnalysis(createResult.projectId, manifest);
|
|
109
|
-
}
|
|
110
|
-
async function runAnalysis(projectId, manifest) {
|
|
111
|
-
// Analyze video
|
|
112
|
-
const descSpinner = ora('Analyzing video...').start();
|
|
95
|
+
// Trigger analysis (runs in background on server)
|
|
96
|
+
const analysisSpinner = ora('Generating narrated tutorial...').start();
|
|
113
97
|
try {
|
|
114
|
-
|
|
115
|
-
projectId,
|
|
116
|
-
videoContext: manifest.videoContext,
|
|
117
|
-
});
|
|
118
|
-
descSpinner.succeed(`Video analyzed (${descResult.description?.length || 0} chars)`);
|
|
119
|
-
}
|
|
120
|
-
catch (err) {
|
|
121
|
-
descSpinner.fail(`Analysis failed: ${err.message}`);
|
|
122
|
-
process.exit(1);
|
|
123
|
-
}
|
|
124
|
-
// Generate narration
|
|
125
|
-
const narrSpinner = ora('Writing narration...').start();
|
|
126
|
-
try {
|
|
127
|
-
const narrResult = await apiCall('generate-narration', {
|
|
128
|
-
projectId,
|
|
98
|
+
await apiCall('generate-video', {
|
|
99
|
+
projectId: createResult.projectId,
|
|
129
100
|
language: manifest.language,
|
|
130
101
|
narrationStyle: manifest.narrationStyle,
|
|
102
|
+
videoContext: manifest.videoContext,
|
|
131
103
|
customPrompt: manifest.customPrompt,
|
|
132
104
|
includeIntro: false,
|
|
133
105
|
});
|
|
134
|
-
narrSpinner.succeed(`${narrResult.segments_created} narration segments`);
|
|
135
|
-
if (narrResult.segments) {
|
|
136
|
-
for (const seg of narrResult.segments) {
|
|
137
|
-
console.log(chalk.dim(` [${seg.sort_order}] ${seg.timestamp_seconds.toFixed(1)}s — ${seg.action_name}`));
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
106
|
}
|
|
141
107
|
catch (err) {
|
|
142
|
-
|
|
108
|
+
analysisSpinner.fail(`Failed to start analysis: ${err.message}`);
|
|
143
109
|
process.exit(1);
|
|
144
110
|
}
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
111
|
+
// Poll until done
|
|
112
|
+
let attempts = 0;
|
|
113
|
+
while (attempts < 120) {
|
|
114
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
115
|
+
const status = await apiCall('get-status', { projectId: createResult.projectId });
|
|
116
|
+
const segCount = status.segments?.length || 0;
|
|
117
|
+
if (status.status === 'editing' || status.status === 'complete') {
|
|
118
|
+
analysisSpinner.succeed(`Done! ${segCount} narration segments`);
|
|
119
|
+
if (status.segments) {
|
|
120
|
+
for (const seg of status.segments) {
|
|
121
|
+
console.log(chalk.dim(` [${seg.sort_order}] ${Number(seg.timestamp_seconds).toFixed(1)}s — ${seg.action_name}`));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
console.log(`\n${chalk.bold('Editor:')} ${status.editor_url}`);
|
|
125
|
+
console.log(chalk.dim('Open the editor to preview narration and generate audio.'));
|
|
126
|
+
saveProjectState({ projectId: createResult.projectId, status: 'complete' });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (status.status === 'draft' || status.status === 'error') {
|
|
130
|
+
analysisSpinner.fail(`Analysis ended with status: ${status.status}`);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
analysisSpinner.text = `Generating narrated tutorial... (${status.status})`;
|
|
134
|
+
attempts++;
|
|
135
|
+
}
|
|
136
|
+
analysisSpinner.fail('Timed out waiting for analysis');
|
|
137
|
+
const finalStatus = await apiCall('get-status', { projectId: createResult.projectId });
|
|
148
138
|
console.log(`${chalk.bold('Editor:')} ${finalStatus.editor_url}`);
|
|
149
|
-
console.log(chalk.dim('Open the editor to preview narration and generate audio.'));
|
|
150
|
-
// Clear state so next run creates a fresh project
|
|
151
|
-
saveProjectState({ projectId, status: 'complete' });
|
|
152
139
|
}
|
|
153
140
|
async function recordAndTrack(manifest) {
|
|
154
141
|
let pw;
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { statusCommand } from './commands/status.js';
|
|
|
7
7
|
const program = new Command()
|
|
8
8
|
.name('vorec')
|
|
9
9
|
.description('Turn screen recordings into narrated tutorials')
|
|
10
|
-
.version('0.
|
|
10
|
+
.version('1.0.0');
|
|
11
11
|
program
|
|
12
12
|
.command('init')
|
|
13
13
|
.description('Save your Vorec API key')
|
|
@@ -27,9 +27,8 @@ program
|
|
|
27
27
|
.action(uploadCommand);
|
|
28
28
|
program
|
|
29
29
|
.command('run <manifest>')
|
|
30
|
-
.description('
|
|
31
|
-
.option('--
|
|
32
|
-
.option('--skip-record', 'Skip Playwright recording (use --video)', false)
|
|
30
|
+
.description('Record screen, upload, and generate narrated tutorial')
|
|
31
|
+
.option('--skip-record', 'Skip recording, use --video instead', false)
|
|
33
32
|
.option('--video <path>', 'Use existing video file instead of recording')
|
|
34
33
|
.option('--tracked-actions <path>', 'JSON file with tracked actions (for --skip-record)')
|
|
35
34
|
.action(runCommand);
|