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