playwright-recast 0.1.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/LICENSE +21 -0
- package/README.md +282 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +139 -0
- package/dist/cli.js.map +1 -0
- package/dist/filter/step-filter.d.ts +7 -0
- package/dist/filter/step-filter.d.ts.map +1 -0
- package/dist/filter/step-filter.js +23 -0
- package/dist/filter/step-filter.js.map +1 -0
- package/dist/helpers.d.ts +41 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +73 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/parse/jsonl-parser.d.ts +87 -0
- package/dist/parse/jsonl-parser.d.ts.map +1 -0
- package/dist/parse/jsonl-parser.js +17 -0
- package/dist/parse/jsonl-parser.js.map +1 -0
- package/dist/parse/trace-parser.d.ts +6 -0
- package/dist/parse/trace-parser.d.ts.map +1 -0
- package/dist/parse/trace-parser.js +149 -0
- package/dist/parse/trace-parser.js.map +1 -0
- package/dist/parse/zip-reader.d.ts +21 -0
- package/dist/parse/zip-reader.d.ts.map +1 -0
- package/dist/parse/zip-reader.js +45 -0
- package/dist/parse/zip-reader.js.map +1 -0
- package/dist/pipeline/executor.d.ts +16 -0
- package/dist/pipeline/executor.d.ts.map +1 -0
- package/dist/pipeline/executor.js +283 -0
- package/dist/pipeline/executor.js.map +1 -0
- package/dist/pipeline/pipeline.d.ts +79 -0
- package/dist/pipeline/pipeline.d.ts.map +1 -0
- package/dist/pipeline/pipeline.js +105 -0
- package/dist/pipeline/pipeline.js.map +1 -0
- package/dist/pipeline/stages.d.ts +49 -0
- package/dist/pipeline/stages.d.ts.map +1 -0
- package/dist/pipeline/stages.js +2 -0
- package/dist/pipeline/stages.js.map +1 -0
- package/dist/render/renderer.d.ts +22 -0
- package/dist/render/renderer.d.ts.map +1 -0
- package/dist/render/renderer.js +174 -0
- package/dist/render/renderer.js.map +1 -0
- package/dist/speed/classifiers.d.ts +8 -0
- package/dist/speed/classifiers.d.ts.map +1 -0
- package/dist/speed/classifiers.js +38 -0
- package/dist/speed/classifiers.js.map +1 -0
- package/dist/speed/speed-processor.d.ts +9 -0
- package/dist/speed/speed-processor.d.ts.map +1 -0
- package/dist/speed/speed-processor.js +97 -0
- package/dist/speed/speed-processor.js.map +1 -0
- package/dist/speed/time-remap.d.ts +12 -0
- package/dist/speed/time-remap.d.ts.map +1 -0
- package/dist/speed/time-remap.js +44 -0
- package/dist/speed/time-remap.js.map +1 -0
- package/dist/subtitles/srt-parser.d.ts +4 -0
- package/dist/subtitles/srt-parser.d.ts.map +1 -0
- package/dist/subtitles/srt-parser.js +28 -0
- package/dist/subtitles/srt-parser.js.map +1 -0
- package/dist/subtitles/srt-writer.d.ts +4 -0
- package/dist/subtitles/srt-writer.d.ts.map +1 -0
- package/dist/subtitles/srt-writer.js +28 -0
- package/dist/subtitles/srt-writer.js.map +1 -0
- package/dist/subtitles/subtitle-generator.d.ts +9 -0
- package/dist/subtitles/subtitle-generator.d.ts.map +1 -0
- package/dist/subtitles/subtitle-generator.js +30 -0
- package/dist/subtitles/subtitle-generator.js.map +1 -0
- package/dist/subtitles/vtt-writer.d.ts +4 -0
- package/dist/subtitles/vtt-writer.d.ts.map +1 -0
- package/dist/subtitles/vtt-writer.js +28 -0
- package/dist/subtitles/vtt-writer.js.map +1 -0
- package/dist/types/render.d.ts +41 -0
- package/dist/types/render.d.ts.map +1 -0
- package/dist/types/render.js +17 -0
- package/dist/types/render.js.map +1 -0
- package/dist/types/speed.d.ts +54 -0
- package/dist/types/speed.d.ts.map +1 -0
- package/dist/types/speed.js +2 -0
- package/dist/types/speed.js.map +1 -0
- package/dist/types/subtitle.d.ts +31 -0
- package/dist/types/subtitle.d.ts.map +1 -0
- package/dist/types/subtitle.js +2 -0
- package/dist/types/subtitle.js.map +1 -0
- package/dist/types/trace.d.ts +103 -0
- package/dist/types/trace.d.ts.map +1 -0
- package/dist/types/trace.js +4 -0
- package/dist/types/trace.js.map +1 -0
- package/dist/types/voiceover.d.ts +42 -0
- package/dist/types/voiceover.d.ts.map +1 -0
- package/dist/types/voiceover.js +2 -0
- package/dist/types/voiceover.js.map +1 -0
- package/dist/utils/ffmpeg.d.ts +7 -0
- package/dist/utils/ffmpeg.d.ts.map +1 -0
- package/dist/utils/ffmpeg.js +24 -0
- package/dist/utils/ffmpeg.js.map +1 -0
- package/dist/voiceover/providers/elevenlabs.d.ts +12 -0
- package/dist/voiceover/providers/elevenlabs.d.ts.map +1 -0
- package/dist/voiceover/providers/elevenlabs.js +55 -0
- package/dist/voiceover/providers/elevenlabs.js.map +1 -0
- package/dist/voiceover/providers/openai.d.ts +14 -0
- package/dist/voiceover/providers/openai.d.ts.map +1 -0
- package/dist/voiceover/providers/openai.js +53 -0
- package/dist/voiceover/providers/openai.js.map +1 -0
- package/dist/voiceover/voiceover-processor.d.ts +9 -0
- package/dist/voiceover/voiceover-processor.d.ts.map +1 -0
- package/dist/voiceover/voiceover-processor.js +88 -0
- package/dist/voiceover/voiceover-processor.js.map +1 -0
- package/package.json +87 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** Raw trace event as parsed from JSONL */
|
|
2
|
+
export type TraceEventRaw = ContextOptionsEvent | BeforeActionEvent | AfterActionEvent | InputEvent | ScreencastFrameEvent | ResourceSnapshotEvent | ConsoleEvent | GenericEvent;
|
|
3
|
+
export interface ContextOptionsEvent {
|
|
4
|
+
type: 'context-options';
|
|
5
|
+
browserName?: string;
|
|
6
|
+
platform?: string;
|
|
7
|
+
options?: {
|
|
8
|
+
viewport?: {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
wallTime?: number;
|
|
14
|
+
monotonicTime?: number;
|
|
15
|
+
playwrightVersion?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface BeforeActionEvent {
|
|
18
|
+
type: 'before';
|
|
19
|
+
callId: string;
|
|
20
|
+
stepId?: string;
|
|
21
|
+
title: string;
|
|
22
|
+
class: string;
|
|
23
|
+
method: string;
|
|
24
|
+
params?: Record<string, unknown>;
|
|
25
|
+
startTime: number;
|
|
26
|
+
pageId?: string;
|
|
27
|
+
parentId?: string;
|
|
28
|
+
stack?: unknown;
|
|
29
|
+
}
|
|
30
|
+
export interface AfterActionEvent {
|
|
31
|
+
type: 'after';
|
|
32
|
+
callId: string;
|
|
33
|
+
endTime: number;
|
|
34
|
+
error?: {
|
|
35
|
+
message: string;
|
|
36
|
+
};
|
|
37
|
+
result?: unknown;
|
|
38
|
+
point?: {
|
|
39
|
+
x: number;
|
|
40
|
+
y: number;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export interface InputEvent {
|
|
44
|
+
type: 'input';
|
|
45
|
+
callId: string;
|
|
46
|
+
point?: {
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export interface ScreencastFrameEvent {
|
|
52
|
+
type: 'screencast-frame';
|
|
53
|
+
pageId: string;
|
|
54
|
+
sha1: string;
|
|
55
|
+
width: number;
|
|
56
|
+
height: number;
|
|
57
|
+
timestamp: number;
|
|
58
|
+
}
|
|
59
|
+
export interface ResourceSnapshotEvent {
|
|
60
|
+
type: 'resource-snapshot';
|
|
61
|
+
snapshot: {
|
|
62
|
+
request: {
|
|
63
|
+
url: string;
|
|
64
|
+
method: string;
|
|
65
|
+
};
|
|
66
|
+
response: {
|
|
67
|
+
status: number;
|
|
68
|
+
mimeType?: string;
|
|
69
|
+
};
|
|
70
|
+
startedDateTime: string;
|
|
71
|
+
time: number;
|
|
72
|
+
_monotonicTime?: number;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export interface ConsoleEvent {
|
|
76
|
+
type: 'console';
|
|
77
|
+
time: number;
|
|
78
|
+
text?: string;
|
|
79
|
+
pageId?: string;
|
|
80
|
+
}
|
|
81
|
+
export interface GenericEvent {
|
|
82
|
+
type: string;
|
|
83
|
+
[key: string]: unknown;
|
|
84
|
+
}
|
|
85
|
+
/** Parse a JSONL string into typed trace events, skipping malformed lines */
|
|
86
|
+
export declare function parseJsonl(content: string): TraceEventRaw[];
|
|
87
|
+
//# sourceMappingURL=jsonl-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonl-parser.d.ts","sourceRoot":"","sources":["../../src/parse/jsonl-parser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Parse a JSONL string into typed trace events, skipping malformed lines */
|
|
2
|
+
export function parseJsonl(content) {
|
|
3
|
+
const events = [];
|
|
4
|
+
for (const line of content.split('\n')) {
|
|
5
|
+
const trimmed = line.trim();
|
|
6
|
+
if (!trimmed)
|
|
7
|
+
continue;
|
|
8
|
+
try {
|
|
9
|
+
events.push(JSON.parse(trimmed));
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// Skip malformed lines
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return events;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=jsonl-parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonl-parser.js","sourceRoot":"","sources":["../../src/parse/jsonl-parser.ts"],"names":[],"mappings":"AAoFA,6EAA6E;AAC7E,MAAM,UAAU,UAAU,CAAC,OAAe;IACxC,MAAM,MAAM,GAAoB,EAAE,CAAA;IAElC,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACvC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAA;QAC3B,IAAI,CAAC,OAAO;YAAE,SAAQ;QACtB,IAAI,CAAC;YACH,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAkB,CAAC,CAAA;QACnD,CAAC;QAAC,MAAM,CAAC;YACP,uBAAuB;QACzB,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace-parser.d.ts","sourceRoot":"","sources":["../../src/parse/trace-parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EAQZ,MAAM,gBAAgB,CAAA;AAcvB;;GAEG;AACH,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAiKxE"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { toMonotonic } from '../types/trace';
|
|
2
|
+
import { ZipReader } from './zip-reader';
|
|
3
|
+
import { parseJsonl, } from './jsonl-parser';
|
|
4
|
+
/**
|
|
5
|
+
* Parse a Playwright trace zip into structured data.
|
|
6
|
+
*/
|
|
7
|
+
export async function parseTrace(tracePath) {
|
|
8
|
+
const zip = ZipReader.open(tracePath);
|
|
9
|
+
const entries = zip.entryNames();
|
|
10
|
+
// Parse ALL trace and network JSONL files (multiple contexts in one zip)
|
|
11
|
+
const traceFiles = entries.filter((n) => n.endsWith('.trace'));
|
|
12
|
+
const networkFiles = entries.filter((n) => n.endsWith('.network'));
|
|
13
|
+
const traceEvents = traceFiles.flatMap((f) => parseJsonl(zip.readText(f)));
|
|
14
|
+
const networkEvents = networkFiles.flatMap((f) => parseJsonl(zip.readText(f)));
|
|
15
|
+
// Find the most informative context-options event (one with browserName set)
|
|
16
|
+
const ctxOptsAll = traceEvents.filter((e) => e.type === 'context-options');
|
|
17
|
+
const ctxOpts = ctxOptsAll.find((e) => e.browserName && e.browserName.length > 0) ??
|
|
18
|
+
ctxOptsAll[0];
|
|
19
|
+
// Build action map: before + after events paired by callId
|
|
20
|
+
const actionStarts = new Map();
|
|
21
|
+
const actionEnds = new Map();
|
|
22
|
+
const inputPoints = new Map();
|
|
23
|
+
for (const event of traceEvents) {
|
|
24
|
+
switch (event.type) {
|
|
25
|
+
case 'before': {
|
|
26
|
+
const e = event;
|
|
27
|
+
actionStarts.set(e.callId, e);
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
case 'after': {
|
|
31
|
+
const e = event;
|
|
32
|
+
actionEnds.set(e.callId, e);
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
case 'input': {
|
|
36
|
+
const e = event;
|
|
37
|
+
if (e.point)
|
|
38
|
+
inputPoints.set(e.callId, e.point);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Build actions
|
|
44
|
+
const actions = [];
|
|
45
|
+
for (const [callId, start] of actionStarts) {
|
|
46
|
+
const end = actionEnds.get(callId);
|
|
47
|
+
const point = inputPoints.get(callId);
|
|
48
|
+
actions.push({
|
|
49
|
+
callId,
|
|
50
|
+
stepId: start.stepId,
|
|
51
|
+
title: start.title,
|
|
52
|
+
class: start.class,
|
|
53
|
+
method: start.method,
|
|
54
|
+
params: start.params ?? {},
|
|
55
|
+
startTime: toMonotonic(start.startTime),
|
|
56
|
+
endTime: toMonotonic(end?.endTime ?? start.startTime),
|
|
57
|
+
parentId: start.parentId,
|
|
58
|
+
error: end?.error,
|
|
59
|
+
point: point
|
|
60
|
+
? { x: point.x, y: point.y, timestamp: toMonotonic(start.startTime) }
|
|
61
|
+
: undefined,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
actions.sort((a, b) => a.startTime - b.startTime);
|
|
65
|
+
// Extract screencast frames
|
|
66
|
+
const frames = traceEvents
|
|
67
|
+
.filter((e) => e.type === 'screencast-frame')
|
|
68
|
+
.map((e) => ({
|
|
69
|
+
sha1: e.sha1,
|
|
70
|
+
timestamp: toMonotonic(e.timestamp),
|
|
71
|
+
pageId: e.pageId,
|
|
72
|
+
width: e.width,
|
|
73
|
+
height: e.height,
|
|
74
|
+
}))
|
|
75
|
+
.sort((a, b) => a.timestamp - b.timestamp);
|
|
76
|
+
// Extract network resources
|
|
77
|
+
const resources = networkEvents
|
|
78
|
+
.filter((e) => e.type === 'resource-snapshot')
|
|
79
|
+
.map((e) => {
|
|
80
|
+
const s = e.snapshot;
|
|
81
|
+
const startTime = s._monotonicTime ?? 0;
|
|
82
|
+
return {
|
|
83
|
+
url: s.request.url,
|
|
84
|
+
method: s.request.method,
|
|
85
|
+
status: s.response.status,
|
|
86
|
+
startTime: toMonotonic(startTime),
|
|
87
|
+
endTime: toMonotonic(startTime + (s.time ?? 0)),
|
|
88
|
+
mimeType: s.response.mimeType ?? '',
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
// Extract cursor positions from input events
|
|
92
|
+
const cursorPositions = [];
|
|
93
|
+
for (const [callId, point] of inputPoints) {
|
|
94
|
+
const start = actionStarts.get(callId);
|
|
95
|
+
if (start) {
|
|
96
|
+
cursorPositions.push({
|
|
97
|
+
x: point.x,
|
|
98
|
+
y: point.y,
|
|
99
|
+
timestamp: toMonotonic(start.startTime),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
cursorPositions.sort((a, b) => a.timestamp - b.timestamp);
|
|
104
|
+
// Extract console/page events
|
|
105
|
+
const events = traceEvents
|
|
106
|
+
.filter((e) => e.type === 'console' || e.type === 'event')
|
|
107
|
+
.map((e) => ({
|
|
108
|
+
type: e.type,
|
|
109
|
+
time: toMonotonic(e.time),
|
|
110
|
+
pageId: e.pageId,
|
|
111
|
+
text: e.text,
|
|
112
|
+
}));
|
|
113
|
+
// Compute time boundaries
|
|
114
|
+
const allTimes = [
|
|
115
|
+
...actions.map((a) => a.startTime),
|
|
116
|
+
...actions.map((a) => a.endTime),
|
|
117
|
+
...frames.map((f) => f.timestamp),
|
|
118
|
+
].filter((t) => t > 0);
|
|
119
|
+
const startTime = allTimes.length > 0 ? Math.min(...allTimes) : 0;
|
|
120
|
+
const endTime = allTimes.length > 0 ? Math.max(...allTimes) : 0;
|
|
121
|
+
// Create frame reader
|
|
122
|
+
const frameReader = {
|
|
123
|
+
readFrame(sha1) {
|
|
124
|
+
const name = `resources/${sha1}`;
|
|
125
|
+
return Promise.resolve(zip.readBinary(name));
|
|
126
|
+
},
|
|
127
|
+
dispose() {
|
|
128
|
+
zip.dispose();
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
return {
|
|
132
|
+
metadata: {
|
|
133
|
+
browserName: ctxOpts?.browserName ?? 'unknown',
|
|
134
|
+
platform: ctxOpts?.platform ?? 'unknown',
|
|
135
|
+
viewport: ctxOpts?.options?.viewport ?? { width: 1920, height: 1080 },
|
|
136
|
+
startTime: toMonotonic(startTime),
|
|
137
|
+
endTime: toMonotonic(endTime),
|
|
138
|
+
wallTime: ctxOpts?.wallTime ?? 0,
|
|
139
|
+
playwrightVersion: ctxOpts?.playwrightVersion,
|
|
140
|
+
},
|
|
141
|
+
frames,
|
|
142
|
+
actions,
|
|
143
|
+
resources,
|
|
144
|
+
events,
|
|
145
|
+
cursorPositions,
|
|
146
|
+
frameReader,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=trace-parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace-parser.js","sourceRoot":"","sources":["../../src/parse/trace-parser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads entries from a Playwright trace ZIP file.
|
|
3
|
+
* Uses fflate (pure JS) for zero-dependency ZIP extraction.
|
|
4
|
+
*/
|
|
5
|
+
export declare class ZipReader {
|
|
6
|
+
private entries;
|
|
7
|
+
private constructor();
|
|
8
|
+
/** Open a zip file and extract all entries into memory */
|
|
9
|
+
static open(zipPath: string): ZipReader;
|
|
10
|
+
/** Get all entry names in the zip */
|
|
11
|
+
entryNames(): string[];
|
|
12
|
+
/** Read an entry as UTF-8 text */
|
|
13
|
+
readText(name: string): string;
|
|
14
|
+
/** Read an entry as raw binary buffer */
|
|
15
|
+
readBinary(name: string): Buffer;
|
|
16
|
+
/** Check if an entry exists */
|
|
17
|
+
has(name: string): boolean;
|
|
18
|
+
/** Release memory */
|
|
19
|
+
dispose(): void;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=zip-reader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zip-reader.d.ts","sourceRoot":"","sources":["../../src/parse/zip-reader.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,OAAO,CAA4B;IAE3C,OAAO;IAIP,0DAA0D;IAC1D,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS;IAMvC,qCAAqC;IACrC,UAAU,IAAI,MAAM,EAAE;IAItB,kCAAkC;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAM9B,yCAAyC;IACzC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAMhC,+BAA+B;IAC/B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI1B,qBAAqB;IACrB,OAAO,IAAI,IAAI;CAGhB"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import { unzipSync, strFromU8 } from 'fflate';
|
|
3
|
+
/**
|
|
4
|
+
* Reads entries from a Playwright trace ZIP file.
|
|
5
|
+
* Uses fflate (pure JS) for zero-dependency ZIP extraction.
|
|
6
|
+
*/
|
|
7
|
+
export class ZipReader {
|
|
8
|
+
entries;
|
|
9
|
+
constructor(entries) {
|
|
10
|
+
this.entries = entries;
|
|
11
|
+
}
|
|
12
|
+
/** Open a zip file and extract all entries into memory */
|
|
13
|
+
static open(zipPath) {
|
|
14
|
+
const data = fs.readFileSync(zipPath);
|
|
15
|
+
const entries = unzipSync(new Uint8Array(data));
|
|
16
|
+
return new ZipReader(entries);
|
|
17
|
+
}
|
|
18
|
+
/** Get all entry names in the zip */
|
|
19
|
+
entryNames() {
|
|
20
|
+
return Object.keys(this.entries);
|
|
21
|
+
}
|
|
22
|
+
/** Read an entry as UTF-8 text */
|
|
23
|
+
readText(name) {
|
|
24
|
+
const entry = this.entries[name];
|
|
25
|
+
if (!entry)
|
|
26
|
+
throw new Error(`Entry not found in zip: ${name}`);
|
|
27
|
+
return strFromU8(entry);
|
|
28
|
+
}
|
|
29
|
+
/** Read an entry as raw binary buffer */
|
|
30
|
+
readBinary(name) {
|
|
31
|
+
const entry = this.entries[name];
|
|
32
|
+
if (!entry)
|
|
33
|
+
throw new Error(`Entry not found in zip: ${name}`);
|
|
34
|
+
return Buffer.from(entry);
|
|
35
|
+
}
|
|
36
|
+
/** Check if an entry exists */
|
|
37
|
+
has(name) {
|
|
38
|
+
return name in this.entries;
|
|
39
|
+
}
|
|
40
|
+
/** Release memory */
|
|
41
|
+
dispose() {
|
|
42
|
+
this.entries = {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=zip-reader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zip-reader.js","sourceRoot":"","sources":["../../src/parse/zip-reader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAA;AAC7B,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAA;AAE7C;;;GAGG;AACH,MAAM,OAAO,SAAS;IACZ,OAAO,CAA4B;IAE3C,YAAoB,OAAmC;QACrD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;IACxB,CAAC;IAED,0DAA0D;IAC1D,MAAM,CAAC,IAAI,CAAC,OAAe;QACzB,MAAM,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,CAAA;QACrC,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAA;QAC/C,OAAO,IAAI,SAAS,CAAC,OAAO,CAAC,CAAA;IAC/B,CAAC;IAED,qCAAqC;IACrC,UAAU;QACR,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAClC,CAAC;IAED,kCAAkC;IAClC,QAAQ,CAAC,IAAY;QACnB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QAChC,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,IAAI,EAAE,CAAC,CAAA;QAC9D,OAAO,SAAS,CAAC,KAAK,CAAC,CAAA;IACzB,CAAC;IAED,yCAAyC;IACzC,UAAU,CAAC,IAAY;QACrB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QAChC,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,IAAI,EAAE,CAAC,CAAA;QAC9D,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC3B,CAAC;IAED,+BAA+B;IAC/B,GAAG,CAAC,IAAY;QACd,OAAO,IAAI,IAAI,IAAI,CAAC,OAAO,CAAA;IAC7B,CAAC;IAED,qBAAqB;IACrB,OAAO;QACL,IAAI,CAAC,OAAO,GAAG,EAAE,CAAA;IACnB,CAAC;CACF"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { StageDescriptor } from './stages';
|
|
2
|
+
/**
|
|
3
|
+
* Executes a pipeline by walking through stages sequentially.
|
|
4
|
+
* Each stage transforms the state into the next type in the chain.
|
|
5
|
+
*/
|
|
6
|
+
export declare class PipelineExecutor {
|
|
7
|
+
private readonly source;
|
|
8
|
+
private readonly stages;
|
|
9
|
+
constructor(source: string, stages: readonly StageDescriptor[]);
|
|
10
|
+
execute(outputPath: string): Promise<void>;
|
|
11
|
+
executeToBuffer(): Promise<Buffer>;
|
|
12
|
+
private runStages;
|
|
13
|
+
private findTraceZip;
|
|
14
|
+
private findSourceVideo;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=executor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/pipeline/executor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAyB/C;;;GAGG;AACH,qBAAa,gBAAgB;IAEzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,SAAS,eAAe,EAAE;IAG/C,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA8D1C,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC;YAQ1B,SAAS;IA6KvB,OAAO,CAAC,YAAY;IAuBpB,OAAO,CAAC,eAAe;CAwBxB"}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { parseTrace } from '../parse/trace-parser';
|
|
4
|
+
import { filterSteps } from '../filter/step-filter';
|
|
5
|
+
import { processSpeed } from '../speed/speed-processor';
|
|
6
|
+
import { generateSubtitles } from '../subtitles/subtitle-generator';
|
|
7
|
+
import { parseSrt } from '../subtitles/srt-parser';
|
|
8
|
+
import { generateVoiceover } from '../voiceover/voiceover-processor';
|
|
9
|
+
import { renderVideo } from '../render/renderer';
|
|
10
|
+
import { writeSrt } from '../subtitles/srt-writer';
|
|
11
|
+
import { writeVtt } from '../subtitles/vtt-writer';
|
|
12
|
+
import { assertFfmpegAvailable } from '../utils/ffmpeg';
|
|
13
|
+
/**
|
|
14
|
+
* Executes a pipeline by walking through stages sequentially.
|
|
15
|
+
* Each stage transforms the state into the next type in the chain.
|
|
16
|
+
*/
|
|
17
|
+
export class PipelineExecutor {
|
|
18
|
+
source;
|
|
19
|
+
stages;
|
|
20
|
+
constructor(source, stages) {
|
|
21
|
+
this.source = source;
|
|
22
|
+
this.stages = stages;
|
|
23
|
+
}
|
|
24
|
+
async execute(outputPath) {
|
|
25
|
+
assertFfmpegAvailable();
|
|
26
|
+
const state = await this.runStages();
|
|
27
|
+
const outputDir = path.dirname(outputPath);
|
|
28
|
+
const tmpDir = path.join(outputDir, '.recast-tmp');
|
|
29
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
30
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
31
|
+
// Find the render config
|
|
32
|
+
const renderStage = this.stages.find((s) => s.type === 'render');
|
|
33
|
+
const renderConfig = renderStage?.type === 'render' ? renderStage.config : {};
|
|
34
|
+
// Determine the most advanced state for rendering
|
|
35
|
+
const renderableTrace = state.voiceovered ?? state.subtitled ?? state.speedMapped ?? state.filtered ?? state.parsed;
|
|
36
|
+
if (!renderableTrace) {
|
|
37
|
+
throw new Error('Pipeline has no data to render. Did you call .parse()?');
|
|
38
|
+
}
|
|
39
|
+
// Build the renderable trace with source video and optional subtitle/voiceover fields
|
|
40
|
+
const traceWithVideo = {
|
|
41
|
+
...renderableTrace,
|
|
42
|
+
sourceVideoPath: state.sourceVideoPath,
|
|
43
|
+
subtitles: state.subtitled?.subtitles,
|
|
44
|
+
voiceover: state.voiceovered?.voiceover,
|
|
45
|
+
};
|
|
46
|
+
// Render final video
|
|
47
|
+
renderVideo(traceWithVideo, renderConfig, outputPath, tmpDir);
|
|
48
|
+
// Write subtitle files next to the output
|
|
49
|
+
if (state.subtitled) {
|
|
50
|
+
const baseName = path.basename(outputPath, path.extname(outputPath));
|
|
51
|
+
const srtPath = path.join(outputDir, `${baseName}.srt`);
|
|
52
|
+
const vttPath = path.join(outputDir, `${baseName}.vtt`);
|
|
53
|
+
fs.writeFileSync(srtPath, writeSrt(state.subtitled.subtitles));
|
|
54
|
+
fs.writeFileSync(vttPath, writeVtt(state.subtitled.subtitles));
|
|
55
|
+
}
|
|
56
|
+
// Write report.json
|
|
57
|
+
if (state.parsed) {
|
|
58
|
+
const reportPath = path.join(outputDir, 'report.json');
|
|
59
|
+
const report = {
|
|
60
|
+
scenario: 'playwright-recast output',
|
|
61
|
+
sourceVideo: state.sourceVideoPath,
|
|
62
|
+
actionsCount: state.parsed.actions.length,
|
|
63
|
+
framesCount: state.parsed.frames.length,
|
|
64
|
+
resourcesCount: state.parsed.resources.length,
|
|
65
|
+
subtitlesCount: state.subtitled?.subtitles.length ?? 0,
|
|
66
|
+
voiceoverSegments: state.voiceovered?.voiceover.entries.length ?? 0,
|
|
67
|
+
outputFile: path.basename(outputPath),
|
|
68
|
+
};
|
|
69
|
+
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + '\n');
|
|
70
|
+
}
|
|
71
|
+
// Cleanup
|
|
72
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
73
|
+
// Dispose frame reader
|
|
74
|
+
state.parsed?.frameReader.dispose();
|
|
75
|
+
}
|
|
76
|
+
async executeToBuffer() {
|
|
77
|
+
const tmpOutput = path.join('/tmp', `recast-${Date.now()}.mp4`);
|
|
78
|
+
await this.execute(tmpOutput);
|
|
79
|
+
const buffer = fs.readFileSync(tmpOutput);
|
|
80
|
+
fs.unlinkSync(tmpOutput);
|
|
81
|
+
return buffer;
|
|
82
|
+
}
|
|
83
|
+
async runStages() {
|
|
84
|
+
const state = {};
|
|
85
|
+
// Find source video in the trace directory
|
|
86
|
+
state.sourceVideoPath = this.findSourceVideo();
|
|
87
|
+
for (const stage of this.stages) {
|
|
88
|
+
switch (stage.type) {
|
|
89
|
+
case 'parse': {
|
|
90
|
+
const tracePath = this.findTraceZip();
|
|
91
|
+
state.parsed = await parseTrace(tracePath);
|
|
92
|
+
// Default filter (no-op) for downstream stages
|
|
93
|
+
state.filtered = {
|
|
94
|
+
...state.parsed,
|
|
95
|
+
originalActions: state.parsed.actions,
|
|
96
|
+
hiddenRanges: [],
|
|
97
|
+
};
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case 'hideSteps': {
|
|
101
|
+
if (!state.parsed)
|
|
102
|
+
throw new Error('hideSteps() requires parse() first');
|
|
103
|
+
state.filtered = filterSteps(state.parsed, stage.predicate);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case 'speedUp': {
|
|
107
|
+
if (!state.filtered)
|
|
108
|
+
throw new Error('speedUp() requires parse() first');
|
|
109
|
+
state.speedMapped = processSpeed(state.filtered, stage.config);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case 'subtitles': {
|
|
113
|
+
// If no speed processing, create a pass-through speed map
|
|
114
|
+
if (!state.speedMapped && state.filtered) {
|
|
115
|
+
state.speedMapped = {
|
|
116
|
+
...state.filtered,
|
|
117
|
+
speedSegments: [],
|
|
118
|
+
timeRemap: (t) => t,
|
|
119
|
+
outputDuration: state.filtered.metadata.endTime - state.filtered.metadata.startTime,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (!state.speedMapped)
|
|
123
|
+
throw new Error('subtitles() requires parse() first');
|
|
124
|
+
state.subtitled = generateSubtitles(state.speedMapped, stage.textFn, stage.options);
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case 'subtitlesFromSrt': {
|
|
128
|
+
// Load subtitles directly from an existing SRT file
|
|
129
|
+
const srtContent = fs.readFileSync(stage.srtPath, 'utf-8');
|
|
130
|
+
const subtitles = parseSrt(srtContent);
|
|
131
|
+
// Promote to SubtitledTrace, filling in any missing intermediate fields
|
|
132
|
+
const srtBase = state.speedMapped ?? state.filtered ?? state.parsed;
|
|
133
|
+
if (!srtBase)
|
|
134
|
+
throw new Error('subtitlesFromSrt() requires parse() first');
|
|
135
|
+
const asFiltered = 'originalActions' in srtBase
|
|
136
|
+
? srtBase
|
|
137
|
+
: { ...srtBase, originalActions: srtBase.actions, hiddenRanges: [] };
|
|
138
|
+
const asSpeedMapped = 'speedSegments' in srtBase
|
|
139
|
+
? srtBase
|
|
140
|
+
: { ...asFiltered, speedSegments: [], timeRemap: (t) => t, outputDuration: 0 };
|
|
141
|
+
state.subtitled = { ...asSpeedMapped, subtitles };
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case 'subtitlesFromTrace': {
|
|
145
|
+
// Generate subtitles from BDD step titles extracted from the parsed trace actions.
|
|
146
|
+
// Uses action.text (BDD step text) or falls back to action.title.
|
|
147
|
+
const traceBase = state.speedMapped ?? state.filtered ?? state.parsed;
|
|
148
|
+
if (!traceBase)
|
|
149
|
+
throw new Error('subtitlesFromTrace() requires parse() first');
|
|
150
|
+
// Ensure we have a SpeedMappedTrace (create pass-through if missing)
|
|
151
|
+
let speedMapped;
|
|
152
|
+
if (state.speedMapped) {
|
|
153
|
+
speedMapped = state.speedMapped;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const filtered = state.filtered ?? {
|
|
157
|
+
...state.parsed,
|
|
158
|
+
originalActions: state.parsed.actions,
|
|
159
|
+
hiddenRanges: [],
|
|
160
|
+
};
|
|
161
|
+
speedMapped = {
|
|
162
|
+
...filtered,
|
|
163
|
+
speedSegments: [],
|
|
164
|
+
timeRemap: (t) => t,
|
|
165
|
+
outputDuration: filtered.metadata.endTime - filtered.metadata.startTime,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// Extract BDD step text from trace actions
|
|
169
|
+
const defaultTextFn = (action) => action.text ?? (action.keyword ? `${action.keyword} ${action.title}` : undefined);
|
|
170
|
+
state.subtitled = generateSubtitles(speedMapped, defaultTextFn, stage.options);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
case 'enrichZoomFromReport': {
|
|
174
|
+
if (!state.subtitled)
|
|
175
|
+
throw new Error('enrichZoomFromReport() requires subtitles() first');
|
|
176
|
+
const reportSteps = stage.steps;
|
|
177
|
+
for (let i = 0; i < Math.min(state.subtitled.subtitles.length, reportSteps.length); i++) {
|
|
178
|
+
const z = reportSteps[i]?.zoom;
|
|
179
|
+
if (z) {
|
|
180
|
+
state.subtitled.subtitles[i].zoom = { x: z.x, y: z.y, level: z.level };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case 'autoZoom': {
|
|
186
|
+
if (!state.subtitled)
|
|
187
|
+
throw new Error('autoZoom() requires subtitles() first');
|
|
188
|
+
if (!state.parsed)
|
|
189
|
+
throw new Error('autoZoom() requires parse() first');
|
|
190
|
+
const actionLevel = stage.config.actionLevel ?? 1.5;
|
|
191
|
+
const viewport = state.parsed.metadata.viewport;
|
|
192
|
+
// Use the first screencast frame timestamp as video t=0.
|
|
193
|
+
// This is the most reliable clock reference for the recording context.
|
|
194
|
+
const firstFrameTime = state.parsed.frames.length > 0
|
|
195
|
+
? state.parsed.frames[0].timestamp
|
|
196
|
+
: state.parsed.metadata.startTime;
|
|
197
|
+
// Find click/fill actions and compute their video-relative time
|
|
198
|
+
const USER_METHODS = new Set(['click', 'fill', 'type', 'press', 'selectOption']);
|
|
199
|
+
const clickActions = state.parsed.actions
|
|
200
|
+
.filter((a) => USER_METHODS.has(a.method))
|
|
201
|
+
.map((a) => ({
|
|
202
|
+
action: a,
|
|
203
|
+
videoTimeSec: (a.startTime - firstFrameTime) / 1000,
|
|
204
|
+
point: a.point,
|
|
205
|
+
}))
|
|
206
|
+
.filter((a) => a.videoTimeSec >= 0);
|
|
207
|
+
for (const subtitle of state.subtitled.subtitles) {
|
|
208
|
+
const subStartSec = subtitle.startMs / 1000;
|
|
209
|
+
const subEndSec = subtitle.endMs / 1000;
|
|
210
|
+
// Find actions with cursor points that fall within this subtitle
|
|
211
|
+
const matching = clickActions.filter((a) => a.videoTimeSec >= subStartSec - 1 && a.videoTimeSec <= subEndSec + 1 && a.point);
|
|
212
|
+
if (matching.length > 0) {
|
|
213
|
+
const best = matching[0];
|
|
214
|
+
subtitle.zoom = {
|
|
215
|
+
x: best.point.x / viewport.width,
|
|
216
|
+
y: best.point.y / viewport.height,
|
|
217
|
+
level: actionLevel,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
case 'voiceover': {
|
|
224
|
+
if (!state.subtitled)
|
|
225
|
+
throw new Error('voiceover() requires subtitles() first');
|
|
226
|
+
const tmpDir = path.join(path.dirname(state.sourceVideoPath ?? '/tmp'), '.recast-vo-tmp');
|
|
227
|
+
state.voiceovered = await generateVoiceover(state.subtitled, stage.provider, tmpDir);
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
case 'render':
|
|
231
|
+
// Config is read during execute(), not here
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return state;
|
|
236
|
+
}
|
|
237
|
+
findTraceZip() {
|
|
238
|
+
// Check if source is a zip file directly
|
|
239
|
+
if (this.source.endsWith('.zip') && fs.existsSync(this.source)) {
|
|
240
|
+
return this.source;
|
|
241
|
+
}
|
|
242
|
+
// Search in directory for trace.zip
|
|
243
|
+
if (fs.existsSync(this.source) && fs.statSync(this.source).isDirectory()) {
|
|
244
|
+
const traceZip = path.join(this.source, 'trace.zip');
|
|
245
|
+
if (fs.existsSync(traceZip))
|
|
246
|
+
return traceZip;
|
|
247
|
+
// Search subdirectories
|
|
248
|
+
for (const entry of fs.readdirSync(this.source, { withFileTypes: true })) {
|
|
249
|
+
if (entry.isDirectory()) {
|
|
250
|
+
const subTrace = path.join(this.source, entry.name, 'trace.zip');
|
|
251
|
+
if (fs.existsSync(subTrace))
|
|
252
|
+
return subTrace;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
throw new Error(`No trace.zip found at: ${this.source}`);
|
|
257
|
+
}
|
|
258
|
+
findSourceVideo() {
|
|
259
|
+
const dir = this.source.endsWith('.zip')
|
|
260
|
+
? path.dirname(this.source)
|
|
261
|
+
: this.source;
|
|
262
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory())
|
|
263
|
+
return undefined;
|
|
264
|
+
// Search for .webm files
|
|
265
|
+
const searchDir = (d) => {
|
|
266
|
+
for (const file of fs.readdirSync(d)) {
|
|
267
|
+
if (file.endsWith('.webm'))
|
|
268
|
+
return path.join(d, file);
|
|
269
|
+
}
|
|
270
|
+
// Check subdirectories
|
|
271
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
272
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
273
|
+
const found = searchDir(path.join(d, entry.name));
|
|
274
|
+
if (found)
|
|
275
|
+
return found;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return undefined;
|
|
279
|
+
};
|
|
280
|
+
return searchDir(dir);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
//# sourceMappingURL=executor.js.map
|