@midscene/cli 1.5.6 → 1.5.7-beta-20260317091411.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/dist/es/index.mjs +272 -4
- package/dist/es/index.mjs.map +1 -1
- package/dist/lib/index.js +272 -4
- package/dist/lib/index.js.map +1 -1
- package/package.json +11 -7
package/dist/es/index.mjs
CHANGED
|
@@ -19,6 +19,11 @@ import { EventEmitter } from "node:events";
|
|
|
19
19
|
import node_stream from "node:stream";
|
|
20
20
|
import { StringDecoder } from "node:string_decoder";
|
|
21
21
|
import { fileURLToPath as external_url_fileURLToPath } from "url";
|
|
22
|
+
import { generatePlaywrightFromVideoFrames, generateYamlFromVideoFrames } from "@midscene/core/ai-model";
|
|
23
|
+
import { globalModelConfigManager } from "@midscene/shared/env";
|
|
24
|
+
import { spawnSync } from "node:child_process";
|
|
25
|
+
import { createRequire } from "node:module";
|
|
26
|
+
import { tmpdir } from "node:os";
|
|
22
27
|
import * as __rspack_external_assert from "assert";
|
|
23
28
|
import * as __rspack_external_crypto from "crypto";
|
|
24
29
|
import * as __rspack_external_fs from "fs";
|
|
@@ -3022,9 +3027,7 @@ var __webpack_modules__ = {
|
|
|
3022
3027
|
"./src/index.ts" (__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
|
|
3023
3028
|
var main = __webpack_require__("../../node_modules/.pnpm/dotenv@16.4.5/node_modules/dotenv/lib/main.js");
|
|
3024
3029
|
var main_default = /*#__PURE__*/ __webpack_require__.n(main);
|
|
3025
|
-
var package_namespaceObject = {
|
|
3026
|
-
rE: "1.5.6"
|
|
3027
|
-
};
|
|
3030
|
+
var package_namespaceObject = JSON.parse('{"rE":"1.5.7-beta-20260317091411.0"}');
|
|
3028
3031
|
class Node {
|
|
3029
3032
|
value;
|
|
3030
3033
|
next;
|
|
@@ -11924,7 +11927,7 @@ Usage:
|
|
|
11924
11927
|
type: 'boolean',
|
|
11925
11928
|
description: `Turn on logging to help debug why certain keys or values are not being set as you expect, default is ${config_factory_defaultConfig.dotenvDebug}`
|
|
11926
11929
|
}
|
|
11927
|
-
}).version('version', 'Show version number', "1.5.
|
|
11930
|
+
}).version('version', 'Show version number', "1.5.7-beta-20260317091411.0").help().epilogue(`For complete list of configuration options, please visit:
|
|
11928
11931
|
• Web options: https://midscenejs.com/automate-with-scripts-in-yaml#the-web-part
|
|
11929
11932
|
• Android options: https://midscenejs.com/automate-with-scripts-in-yaml#the-android-part
|
|
11930
11933
|
• iOS options: https://midscenejs.com/automate-with-scripts-in-yaml#the-ios-part
|
|
@@ -11972,7 +11975,272 @@ Examples:
|
|
|
11972
11975
|
});
|
|
11973
11976
|
return files.filter((file)=>file.endsWith('.yml') || file.endsWith('.yaml')).sort();
|
|
11974
11977
|
}
|
|
11978
|
+
function consumeArg(args, index, flag) {
|
|
11979
|
+
const value = args[index + 1];
|
|
11980
|
+
if (void 0 === value || value.startsWith('-')) {
|
|
11981
|
+
console.error(`Error: ${flag} requires a value`);
|
|
11982
|
+
process.exit(1);
|
|
11983
|
+
}
|
|
11984
|
+
return value;
|
|
11985
|
+
}
|
|
11986
|
+
function parseVideo2YamlArgs(args) {
|
|
11987
|
+
const options = {
|
|
11988
|
+
input: ''
|
|
11989
|
+
};
|
|
11990
|
+
let i = 0;
|
|
11991
|
+
while(i < args.length){
|
|
11992
|
+
const arg = args[i];
|
|
11993
|
+
if ('--output' === arg || '-o' === arg) {
|
|
11994
|
+
options.output = consumeArg(args, i, arg);
|
|
11995
|
+
i++;
|
|
11996
|
+
} else if ('--format' === arg || '-f' === arg) {
|
|
11997
|
+
const fmt = consumeArg(args, i, arg);
|
|
11998
|
+
if ('yaml' !== fmt && 'playwright' !== fmt) {
|
|
11999
|
+
console.error(`Invalid format: ${fmt}. Must be "yaml" or "playwright"`);
|
|
12000
|
+
process.exit(1);
|
|
12001
|
+
}
|
|
12002
|
+
options.format = fmt;
|
|
12003
|
+
i++;
|
|
12004
|
+
} else if ('--url' === arg) {
|
|
12005
|
+
options.url = consumeArg(args, i, arg);
|
|
12006
|
+
i++;
|
|
12007
|
+
} else if ("--description" === arg) {
|
|
12008
|
+
options.description = consumeArg(args, i, arg);
|
|
12009
|
+
i++;
|
|
12010
|
+
} else if ('--fps' === arg) {
|
|
12011
|
+
options.fps = Number(consumeArg(args, i, arg));
|
|
12012
|
+
i++;
|
|
12013
|
+
} else if ('--max-frames' === arg) {
|
|
12014
|
+
options.maxFrames = Number(consumeArg(args, i, arg));
|
|
12015
|
+
i++;
|
|
12016
|
+
} else if ('--viewport-width' === arg) {
|
|
12017
|
+
options.viewportWidth = Number(consumeArg(args, i, arg));
|
|
12018
|
+
i++;
|
|
12019
|
+
} else if ('--viewport-height' === arg) {
|
|
12020
|
+
options.viewportHeight = Number(consumeArg(args, i, arg));
|
|
12021
|
+
i++;
|
|
12022
|
+
} else if ('--help' === arg || '-h' === arg) {
|
|
12023
|
+
printHelp();
|
|
12024
|
+
process.exit(0);
|
|
12025
|
+
} else if (arg.startsWith('-') || options.input) {
|
|
12026
|
+
console.error(`Unknown option: ${arg}`);
|
|
12027
|
+
printHelp();
|
|
12028
|
+
process.exit(1);
|
|
12029
|
+
} else options.input = arg;
|
|
12030
|
+
i++;
|
|
12031
|
+
}
|
|
12032
|
+
if (!options.input) {
|
|
12033
|
+
console.error('Error: video file path is required');
|
|
12034
|
+
printHelp();
|
|
12035
|
+
process.exit(1);
|
|
12036
|
+
}
|
|
12037
|
+
return options;
|
|
12038
|
+
}
|
|
12039
|
+
function printHelp() {
|
|
12040
|
+
console.log(`
|
|
12041
|
+
Usage: midscene video2yaml <video-file> [options]
|
|
12042
|
+
|
|
12043
|
+
Generate a runnable Midscene test script from a screen recording video.
|
|
12044
|
+
|
|
12045
|
+
Arguments:
|
|
12046
|
+
video-file Path to the video file (mp4, webm, etc.)
|
|
12047
|
+
|
|
12048
|
+
Options:
|
|
12049
|
+
-o, --output <path> Output file path (default: <video-file>.yaml or .test.ts)
|
|
12050
|
+
-f, --format <format> Output format: "yaml" (default) or "playwright"
|
|
12051
|
+
--url <url> Starting URL of the web page in the video
|
|
12052
|
+
--description <text> Description of what the video demonstrates
|
|
12053
|
+
--fps <number> Frames per second to extract (default: 1)
|
|
12054
|
+
--max-frames <number> Maximum number of frames to analyze (default: 20)
|
|
12055
|
+
--viewport-width <px> Viewport width of the recorded page
|
|
12056
|
+
--viewport-height <px> Viewport height of the recorded page
|
|
12057
|
+
-h, --help Show this help message
|
|
12058
|
+
|
|
12059
|
+
Examples:
|
|
12060
|
+
# Generate YAML script (default)
|
|
12061
|
+
midscene video2yaml recording.mp4
|
|
12062
|
+
midscene video2yaml recording.mp4 -o test.yaml --url https://example.com
|
|
12063
|
+
|
|
12064
|
+
# Generate Playwright test
|
|
12065
|
+
midscene video2yaml recording.mp4 --format playwright
|
|
12066
|
+
midscene video2yaml recording.mp4 -f playwright -o login.test.ts
|
|
12067
|
+
|
|
12068
|
+
# Custom frame extraction
|
|
12069
|
+
midscene video2yaml demo.webm --fps 2 --max-frames 30 --description "Login flow test"
|
|
12070
|
+
`);
|
|
12071
|
+
}
|
|
12072
|
+
const extract_frames_debug = getDebug('cli:video');
|
|
12073
|
+
function resolveExecutable(npmPackage, systemFallback) {
|
|
12074
|
+
try {
|
|
12075
|
+
const dynamicRequire = createRequire(__filename);
|
|
12076
|
+
const installer = dynamicRequire(npmPackage);
|
|
12077
|
+
const binPath = installer.path;
|
|
12078
|
+
try {
|
|
12079
|
+
(0, __rspack_external_node_fs_5ea92f0c.chmodSync)(binPath, 493);
|
|
12080
|
+
} catch {}
|
|
12081
|
+
const check = spawnSync(binPath, [
|
|
12082
|
+
'-version'
|
|
12083
|
+
], {
|
|
12084
|
+
stdio: 'pipe',
|
|
12085
|
+
timeout: 5000
|
|
12086
|
+
});
|
|
12087
|
+
if (check.error || 0 !== check.status) {
|
|
12088
|
+
extract_frames_debug(`npm ${npmPackage} binary not executable (${check.error?.message ?? `status ${check.status}`}), falling back to system ${systemFallback}`);
|
|
12089
|
+
return systemFallback;
|
|
12090
|
+
}
|
|
12091
|
+
extract_frames_debug(`Using ${systemFallback} from npm package: ${binPath}`);
|
|
12092
|
+
return binPath;
|
|
12093
|
+
} catch (error) {
|
|
12094
|
+
extract_frames_debug(`npm ${npmPackage} not found (${error}), falling back to system ${systemFallback}`);
|
|
12095
|
+
return systemFallback;
|
|
12096
|
+
}
|
|
12097
|
+
}
|
|
12098
|
+
function getFfmpegPath() {
|
|
12099
|
+
return resolveExecutable('@ffmpeg-installer/ffmpeg', 'ffmpeg');
|
|
12100
|
+
}
|
|
12101
|
+
function getFfprobePath() {
|
|
12102
|
+
return resolveExecutable('@ffprobe-installer/ffprobe', 'ffprobe');
|
|
12103
|
+
}
|
|
12104
|
+
function checkFfmpeg() {
|
|
12105
|
+
try {
|
|
12106
|
+
const ffmpegPath = getFfmpegPath();
|
|
12107
|
+
const result = spawnSync(ffmpegPath, [
|
|
12108
|
+
'-version'
|
|
12109
|
+
], {
|
|
12110
|
+
stdio: 'pipe',
|
|
12111
|
+
timeout: 5000
|
|
12112
|
+
});
|
|
12113
|
+
extract_frames_debug(`ffmpeg check result: status=${result.status}`);
|
|
12114
|
+
return 0 === result.status;
|
|
12115
|
+
} catch (error) {
|
|
12116
|
+
extract_frames_debug(`ffmpeg check failed: ${error}`);
|
|
12117
|
+
return false;
|
|
12118
|
+
}
|
|
12119
|
+
}
|
|
12120
|
+
function getVideoDuration(videoPath) {
|
|
12121
|
+
const ffprobePath = getFfprobePath();
|
|
12122
|
+
const result = spawnSync(ffprobePath, [
|
|
12123
|
+
'-v',
|
|
12124
|
+
'error',
|
|
12125
|
+
'-show_entries',
|
|
12126
|
+
'format=duration',
|
|
12127
|
+
'-of',
|
|
12128
|
+
'csv=p=0',
|
|
12129
|
+
videoPath
|
|
12130
|
+
], {
|
|
12131
|
+
stdio: 'pipe',
|
|
12132
|
+
timeout: 10000
|
|
12133
|
+
});
|
|
12134
|
+
if (result.error) throw new Error(`Failed to run ffprobe: ${result.error.message}`);
|
|
12135
|
+
if (0 !== result.status) throw new Error(`Failed to get video duration: ${result.stderr?.toString() || 'unknown error'}`);
|
|
12136
|
+
const duration = Number.parseFloat(result.stdout.toString().trim());
|
|
12137
|
+
if (Number.isNaN(duration)) throw new Error('Could not parse video duration');
|
|
12138
|
+
return duration;
|
|
12139
|
+
}
|
|
12140
|
+
function extractFrames(videoPath, options) {
|
|
12141
|
+
const { fps = 1, maxFrames, width } = options;
|
|
12142
|
+
if (!(0, __rspack_external_node_fs_5ea92f0c.existsSync)(videoPath)) throw new Error(`Video file not found: ${videoPath}`);
|
|
12143
|
+
if (!checkFfmpeg()) throw new Error("ffmpeg is not available.\nTo fix this, either:\n 1. Install the npm package: pnpm add -D @ffmpeg-installer/ffmpeg\n 2. Or install system ffmpeg: https://ffmpeg.org/download.html");
|
|
12144
|
+
const duration = getVideoDuration(videoPath);
|
|
12145
|
+
const totalFramesAtFps = Math.ceil(duration * fps);
|
|
12146
|
+
const actualFps = totalFramesAtFps > maxFrames ? maxFrames / duration : fps;
|
|
12147
|
+
const tempDir = join(tmpdir(), `midscene-video-frames-${Date.now()}`);
|
|
12148
|
+
(0, __rspack_external_node_fs_5ea92f0c.mkdirSync)(tempDir, {
|
|
12149
|
+
recursive: true
|
|
12150
|
+
});
|
|
12151
|
+
try {
|
|
12152
|
+
const filters = [
|
|
12153
|
+
`fps=${actualFps}`
|
|
12154
|
+
];
|
|
12155
|
+
if (width) filters.push(`scale=${Math.min(width, 1920)}:-1`);
|
|
12156
|
+
else filters.push('scale=min(iw\\,1920):-1');
|
|
12157
|
+
const ffmpegPath = getFfmpegPath();
|
|
12158
|
+
const ffmpegArgs = [
|
|
12159
|
+
'-i',
|
|
12160
|
+
videoPath,
|
|
12161
|
+
'-vf',
|
|
12162
|
+
filters.join(','),
|
|
12163
|
+
'-q:v',
|
|
12164
|
+
'2',
|
|
12165
|
+
'-frames:v',
|
|
12166
|
+
String(maxFrames),
|
|
12167
|
+
join(tempDir, 'frame_%04d.jpg')
|
|
12168
|
+
];
|
|
12169
|
+
const result = spawnSync(ffmpegPath, ffmpegArgs, {
|
|
12170
|
+
stdio: 'pipe',
|
|
12171
|
+
timeout: 120000
|
|
12172
|
+
});
|
|
12173
|
+
if (result.error) throw new Error(`ffmpeg execution error: ${result.error.message}`);
|
|
12174
|
+
if (0 !== result.status) throw new Error(`ffmpeg failed: ${result.stderr?.toString() || 'unknown error'}`);
|
|
12175
|
+
const frameFiles = (0, __rspack_external_node_fs_5ea92f0c.readdirSync)(tempDir).filter((f)=>f.startsWith('frame_') && f.endsWith('.jpg')).sort();
|
|
12176
|
+
const frames = [];
|
|
12177
|
+
for(let i = 0; i < frameFiles.length; i++){
|
|
12178
|
+
const filePath = join(tempDir, frameFiles[i]);
|
|
12179
|
+
const buffer = (0, __rspack_external_node_fs_5ea92f0c.readFileSync)(filePath);
|
|
12180
|
+
const base64 = buffer.toString('base64');
|
|
12181
|
+
const timestamp = i / actualFps;
|
|
12182
|
+
frames.push({
|
|
12183
|
+
base64: `data:image/jpeg;base64,${base64}`,
|
|
12184
|
+
timestamp
|
|
12185
|
+
});
|
|
12186
|
+
}
|
|
12187
|
+
return frames;
|
|
12188
|
+
} finally{
|
|
12189
|
+
try {
|
|
12190
|
+
(0, __rspack_external_node_fs_5ea92f0c.rmSync)(tempDir, {
|
|
12191
|
+
recursive: true,
|
|
12192
|
+
force: true
|
|
12193
|
+
});
|
|
12194
|
+
} catch {}
|
|
12195
|
+
}
|
|
12196
|
+
}
|
|
12197
|
+
function getDefaultOutputPath(inputPath, format) {
|
|
12198
|
+
const ext = 'playwright' === format ? '.test.ts' : '.yaml';
|
|
12199
|
+
return inputPath.replace(/\.[^.]+$/, ext);
|
|
12200
|
+
}
|
|
12201
|
+
async function video2yaml(options) {
|
|
12202
|
+
const { input, output, format = 'yaml', url, description, fps = 1, maxFrames = 20, viewportWidth, viewportHeight } = options;
|
|
12203
|
+
const inputPath = external_node_path_resolve(input);
|
|
12204
|
+
if (!(0, __rspack_external_node_fs_5ea92f0c.existsSync)(inputPath)) throw new Error(`Video file not found: ${inputPath}`);
|
|
12205
|
+
const outputPath = output ? external_node_path_resolve(output) : getDefaultOutputPath(inputPath, format);
|
|
12206
|
+
console.log(`\n Extracting frames from video (fps=${fps}, max=${maxFrames})...`);
|
|
12207
|
+
const frames = extractFrames(inputPath, {
|
|
12208
|
+
fps,
|
|
12209
|
+
maxFrames
|
|
12210
|
+
});
|
|
12211
|
+
console.log(` Extracted ${frames.length} frames`);
|
|
12212
|
+
if (0 === frames.length) throw new Error('No frames could be extracted from the video');
|
|
12213
|
+
const formatLabel = 'playwright' === format ? 'Playwright test' : 'YAML';
|
|
12214
|
+
console.log(` Analyzing video frames with AI (generating ${formatLabel})...`);
|
|
12215
|
+
const modelConfig = globalModelConfigManager.getModelConfig('default');
|
|
12216
|
+
const scriptOptions = {
|
|
12217
|
+
url,
|
|
12218
|
+
description,
|
|
12219
|
+
viewportWidth,
|
|
12220
|
+
viewportHeight
|
|
12221
|
+
};
|
|
12222
|
+
const result = 'playwright' === format ? await generatePlaywrightFromVideoFrames(frames, scriptOptions, modelConfig) : await generateYamlFromVideoFrames(frames, scriptOptions, modelConfig);
|
|
12223
|
+
(0, __rspack_external_node_fs_5ea92f0c.writeFileSync)(outputPath, result.content, 'utf-8');
|
|
12224
|
+
console.log(` ${formatLabel} script saved to: ${outputPath}`);
|
|
12225
|
+
return outputPath;
|
|
12226
|
+
}
|
|
12227
|
+
async function handleVideo2Yaml(args) {
|
|
12228
|
+
const options = parseVideo2YamlArgs(args);
|
|
12229
|
+
const dotEnvConfigFile = join(process.cwd(), '.env');
|
|
12230
|
+
if ((0, __rspack_external_node_fs_5ea92f0c.existsSync)(dotEnvConfigFile)) main_default().config({
|
|
12231
|
+
path: dotEnvConfigFile
|
|
12232
|
+
});
|
|
12233
|
+
await video2yaml(options);
|
|
12234
|
+
}
|
|
11975
12235
|
Promise.resolve((async ()=>{
|
|
12236
|
+
const rawArgs = process.argv.slice(2);
|
|
12237
|
+
if ('video2yaml' === rawArgs[0]) {
|
|
12238
|
+
const welcome = `\nWelcome to @midscene/cli v${package_namespaceObject.rE}\n`;
|
|
12239
|
+
console.log(welcome);
|
|
12240
|
+
await handleVideo2Yaml(rawArgs.slice(1));
|
|
12241
|
+
process.exit(0);
|
|
12242
|
+
return;
|
|
12243
|
+
}
|
|
11976
12244
|
const { options, path, files: cmdFiles } = await parseProcessArgs();
|
|
11977
12245
|
const welcome = `\nWelcome to @midscene/cli v${package_namespaceObject.rE}\n`;
|
|
11978
12246
|
console.log(welcome);
|