@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 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.6").help().epilogue(`For complete list of configuration options, please visit:
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);