@empiricalrun/test-gen 0.76.0 → 0.77.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.
Files changed (230) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/agent/base/index.d.ts +25 -21
  3. package/dist/agent/base/index.d.ts.map +1 -1
  4. package/dist/agent/base/index.js +48 -37
  5. package/dist/agent/browsing/run.d.ts +1 -2
  6. package/dist/agent/browsing/run.d.ts.map +1 -1
  7. package/dist/agent/browsing/run.js +3 -9
  8. package/dist/agent/browsing/utils.d.ts +2 -9
  9. package/dist/agent/browsing/utils.d.ts.map +1 -1
  10. package/dist/agent/browsing/utils.js +5 -109
  11. package/dist/agent/chat/agent-loop.d.ts +5 -5
  12. package/dist/agent/chat/agent-loop.d.ts.map +1 -1
  13. package/dist/agent/chat/agent-loop.js +3 -8
  14. package/dist/agent/chat/exports.d.ts +5 -4
  15. package/dist/agent/chat/exports.d.ts.map +1 -1
  16. package/dist/agent/chat/exports.js +4 -7
  17. package/dist/agent/chat/index.d.ts +2 -2
  18. package/dist/agent/chat/index.d.ts.map +1 -1
  19. package/dist/agent/chat/index.js +23 -35
  20. package/dist/agent/chat/models.d.ts +0 -2
  21. package/dist/agent/chat/models.d.ts.map +1 -1
  22. package/dist/agent/chat/models.js +12 -26
  23. package/dist/agent/chat/prompt/pw-utils-docs.d.ts +1 -1
  24. package/dist/agent/chat/prompt/pw-utils-docs.d.ts.map +1 -1
  25. package/dist/agent/chat/prompt/pw-utils-docs.js +52 -0
  26. package/dist/agent/chat/prompt/repo.d.ts.map +1 -1
  27. package/dist/agent/chat/prompt/repo.js +11 -22
  28. package/dist/agent/chat/prompt/test-case-def.d.ts +2 -0
  29. package/dist/agent/chat/prompt/test-case-def.d.ts.map +1 -0
  30. package/dist/agent/chat/prompt/test-case-def.js +44 -0
  31. package/dist/agent/chat/state.d.ts +7 -6
  32. package/dist/agent/chat/state.d.ts.map +1 -1
  33. package/dist/agent/chat/state.js +15 -45
  34. package/dist/agent/chat/utils.d.ts +2 -2
  35. package/dist/agent/chat/utils.d.ts.map +1 -1
  36. package/dist/agent/chat/utils.js +14 -7
  37. package/dist/agent/cli.d.ts.map +1 -1
  38. package/dist/agent/cli.js +62 -58
  39. package/dist/agent/code-review/executor/index.d.ts +5 -0
  40. package/dist/agent/code-review/executor/index.d.ts.map +1 -0
  41. package/dist/agent/code-review/executor/index.js +13 -0
  42. package/dist/agent/code-review/index.d.ts +8 -3
  43. package/dist/agent/code-review/index.d.ts.map +1 -1
  44. package/dist/agent/code-review/index.js +115 -21
  45. package/dist/agent/code-review/parser.d.ts +5 -0
  46. package/dist/agent/code-review/parser.d.ts.map +1 -0
  47. package/dist/agent/code-review/parser.js +70 -0
  48. package/dist/agent/code-review/types.d.ts +36 -0
  49. package/dist/agent/code-review/types.d.ts.map +1 -0
  50. package/dist/agent/code-review/types.js +13 -0
  51. package/dist/agent/cua/index.d.ts.map +1 -1
  52. package/dist/agent/cua/index.js +18 -2
  53. package/dist/agent/cua/model.d.ts.map +1 -1
  54. package/dist/agent/cua/model.js +4 -1
  55. package/dist/agent/cua/pw-codegen/pw-pause/index.d.ts.map +1 -1
  56. package/dist/agent/triage/index.d.ts +2 -2
  57. package/dist/agent/triage/index.d.ts.map +1 -1
  58. package/dist/agent/triage/index.js +8 -7
  59. package/dist/agent/video-analysis/executor/index.d.ts +5 -0
  60. package/dist/agent/video-analysis/executor/index.d.ts.map +1 -0
  61. package/dist/agent/video-analysis/executor/index.js +10 -0
  62. package/dist/agent/video-analysis/index.d.ts +2 -2
  63. package/dist/agent/video-analysis/index.d.ts.map +1 -1
  64. package/dist/agent/video-analysis/index.js +38 -13
  65. package/dist/artifacts/index.d.ts +1 -1
  66. package/dist/artifacts/index.d.ts.map +1 -1
  67. package/dist/artifacts/index.js +3 -1
  68. package/dist/artifacts/utils.d.ts.map +1 -1
  69. package/dist/bin/index.js +66 -21
  70. package/dist/constants/index.d.ts +14 -0
  71. package/dist/constants/index.d.ts.map +1 -1
  72. package/dist/constants/index.js +33 -1
  73. package/dist/file/server.d.ts +1 -3
  74. package/dist/file/server.d.ts.map +1 -1
  75. package/dist/file/server.js +0 -13
  76. package/dist/file-info/adapters/file-system/index.d.ts.map +1 -1
  77. package/dist/file-info/adapters/file-system/reader.d.ts.map +1 -1
  78. package/dist/file-info/adapters/file-system/reader.js +8 -1
  79. package/dist/file-info/adapters/github/index.d.ts.map +1 -1
  80. package/dist/file-info/adapters/github/reader.d.ts +1 -1
  81. package/dist/file-info/adapters/github/reader.d.ts.map +1 -1
  82. package/dist/file-info/adapters/github/reader.js +8 -5
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/tools/analyse-video/index.d.ts +5 -0
  85. package/dist/tools/analyse-video/index.d.ts.map +1 -0
  86. package/dist/tools/analyse-video/index.js +50 -0
  87. package/dist/tools/create-pull-request/index.js +4 -6
  88. package/dist/tools/create-pull-request/utils.d.ts +1 -1
  89. package/dist/tools/definitions/{fetch-video-analysis.d.ts → analyse-video.d.ts} +13 -8
  90. package/dist/tools/definitions/analyse-video.d.ts.map +1 -0
  91. package/dist/tools/definitions/analyse-video.js +60 -0
  92. package/dist/tools/definitions/review-pull-request.d.ts +3 -0
  93. package/dist/tools/definitions/review-pull-request.d.ts.map +1 -0
  94. package/dist/tools/definitions/review-pull-request.js +16 -0
  95. package/dist/tools/definitions/str_replace_editor.d.ts +1 -0
  96. package/dist/tools/definitions/str_replace_editor.d.ts.map +1 -1
  97. package/dist/tools/definitions/str_replace_editor.js +4 -1
  98. package/dist/tools/definitions/test-gen-browser.d.ts +0 -3
  99. package/dist/tools/definitions/test-gen-browser.d.ts.map +1 -1
  100. package/dist/tools/definitions/test-gen-browser.js +33 -8
  101. package/dist/tools/delete-file/index.d.ts.map +1 -1
  102. package/dist/tools/delete-file/index.js +1 -19
  103. package/dist/tools/executor/base.d.ts +32 -0
  104. package/dist/tools/executor/base.d.ts.map +1 -0
  105. package/dist/tools/executor/base.js +114 -0
  106. package/dist/tools/executor/index.d.ts +3 -22
  107. package/dist/tools/executor/index.d.ts.map +1 -1
  108. package/dist/tools/executor/index.js +7 -100
  109. package/dist/tools/executor/utils/checkpoint.d.ts +1 -1
  110. package/dist/tools/executor/utils/checkpoint.d.ts.map +1 -1
  111. package/dist/tools/executor/utils/checkpoint.js +6 -2
  112. package/dist/tools/executor/utils/git.d.ts +2 -2
  113. package/dist/tools/executor/utils/git.d.ts.map +1 -1
  114. package/dist/tools/executor/utils/git.js +7 -3
  115. package/dist/tools/executor/utils/index.d.ts.map +1 -1
  116. package/dist/tools/executor/utils/index.js +1 -1
  117. package/dist/tools/fetch-session-diff/index.js +2 -2
  118. package/dist/tools/file-operations/create.d.ts.map +1 -1
  119. package/dist/tools/file-operations/create.js +1 -4
  120. package/dist/tools/file-operations/index.d.ts +2 -1
  121. package/dist/tools/file-operations/index.d.ts.map +1 -1
  122. package/dist/tools/file-operations/index.js +4 -1
  123. package/dist/tools/file-operations/insert.d.ts +1 -2
  124. package/dist/tools/file-operations/insert.d.ts.map +1 -1
  125. package/dist/tools/file-operations/insert.js +1 -4
  126. package/dist/tools/file-operations/replace.d.ts.map +1 -1
  127. package/dist/tools/file-operations/replace.js +1 -4
  128. package/dist/tools/grep/index.d.ts.map +1 -1
  129. package/dist/tools/grep/index.js +18 -11
  130. package/dist/tools/index.d.ts +5 -5
  131. package/dist/tools/index.d.ts.map +1 -1
  132. package/dist/tools/index.js +17 -16
  133. package/dist/tools/merge-conflicts/index.d.ts.map +1 -1
  134. package/dist/tools/merge-conflicts/index.js +1 -1
  135. package/dist/tools/rename-file/index.js +1 -1
  136. package/dist/tools/review-pull-request/index.d.ts.map +1 -1
  137. package/dist/tools/review-pull-request/index.js +45 -59
  138. package/dist/tools/run-test.d.ts.map +1 -1
  139. package/dist/tools/run-test.js +25 -3
  140. package/dist/tools/test-gen-browser.d.ts.map +1 -1
  141. package/dist/tools/test-gen-browser.js +51 -47
  142. package/dist/utils/artifact-paths.d.ts +20 -0
  143. package/dist/utils/artifact-paths.d.ts.map +1 -0
  144. package/dist/utils/artifact-paths.js +16 -0
  145. package/dist/utils/dedup-image-fs.d.ts +2 -16
  146. package/dist/utils/dedup-image-fs.d.ts.map +1 -1
  147. package/dist/utils/dedup-image-fs.js +12 -16
  148. package/dist/utils/dedup-image.d.ts +1 -14
  149. package/dist/utils/dedup-image.d.ts.map +1 -1
  150. package/dist/utils/dedup-image.js +7 -62
  151. package/dist/utils/{local-ffmpeg-client.d.ts → ffmpeg/index.d.ts} +6 -7
  152. package/dist/utils/ffmpeg/index.d.ts.map +1 -0
  153. package/dist/utils/{local-ffmpeg-client.js → ffmpeg/index.js} +169 -53
  154. package/dist/utils/find-threshold.d.ts +8 -0
  155. package/dist/utils/find-threshold.d.ts.map +1 -0
  156. package/dist/utils/find-threshold.js +55 -0
  157. package/dist/utils/hash.d.ts +2 -0
  158. package/dist/utils/hash.d.ts.map +1 -0
  159. package/dist/utils/hash.js +24 -0
  160. package/dist/utils/model.d.ts +1 -1
  161. package/dist/utils/model.d.ts.map +1 -1
  162. package/dist/utils/model.js +7 -5
  163. package/dist/utils/repo-tree.d.ts +0 -1
  164. package/dist/utils/repo-tree.d.ts.map +1 -1
  165. package/dist/utils/repo-tree.js +2 -14
  166. package/dist/utils/slug.js +1 -1
  167. package/dist/video-core/agent-orchestrator.d.ts +14 -0
  168. package/dist/video-core/agent-orchestrator.d.ts.map +1 -0
  169. package/dist/video-core/agent-orchestrator.js +78 -0
  170. package/dist/video-core/analysis-server.d.ts +24 -0
  171. package/dist/video-core/analysis-server.d.ts.map +1 -0
  172. package/dist/video-core/analysis-server.js +398 -0
  173. package/dist/video-core/analysis-viewer.html +1374 -0
  174. package/dist/video-core/index.d.ts +44 -0
  175. package/dist/video-core/index.d.ts.map +1 -0
  176. package/dist/video-core/index.js +204 -0
  177. package/dist/video-core/model-limits.d.ts +4 -0
  178. package/dist/video-core/model-limits.d.ts.map +1 -0
  179. package/dist/video-core/model-limits.js +67 -0
  180. package/dist/video-core/storage-manager.d.ts +5 -0
  181. package/dist/video-core/storage-manager.d.ts.map +1 -0
  182. package/dist/video-core/storage-manager.js +55 -0
  183. package/dist/video-core/types.d.ts +13 -0
  184. package/dist/video-core/types.d.ts.map +1 -0
  185. package/dist/video-core/types.js +2 -0
  186. package/dist/video-core/utils.d.ts +25 -0
  187. package/dist/video-core/utils.d.ts.map +1 -0
  188. package/dist/video-core/utils.js +211 -0
  189. package/dist/video-core/xml-parser.d.ts +3 -0
  190. package/dist/video-core/xml-parser.d.ts.map +1 -0
  191. package/dist/video-core/xml-parser.js +27 -0
  192. package/package.json +5 -6
  193. package/tsconfig.tsbuildinfo +1 -1
  194. package/dist/agent/chat/prompt/index.d.ts +0 -6
  195. package/dist/agent/chat/prompt/index.d.ts.map +0 -1
  196. package/dist/agent/chat/prompt/index.js +0 -200
  197. package/dist/agent/code-review/prompt.d.ts +0 -2
  198. package/dist/agent/code-review/prompt.d.ts.map +0 -1
  199. package/dist/agent/code-review/prompt.js +0 -55
  200. package/dist/agent/diagnosis-agent/index.d.ts +0 -11
  201. package/dist/agent/diagnosis-agent/index.d.ts.map +0 -1
  202. package/dist/agent/diagnosis-agent/index.js +0 -88
  203. package/dist/agent/diagnosis-agent/strict-mode-violation.d.ts +0 -10
  204. package/dist/agent/diagnosis-agent/strict-mode-violation.d.ts.map +0 -1
  205. package/dist/agent/diagnosis-agent/strict-mode-violation.js +0 -30
  206. package/dist/tools/definitions/extract-frames-from-video.d.ts +0 -39
  207. package/dist/tools/definitions/extract-frames-from-video.d.ts.map +0 -1
  208. package/dist/tools/definitions/extract-frames-from-video.js +0 -60
  209. package/dist/tools/definitions/fetch-video-analysis.d.ts.map +0 -1
  210. package/dist/tools/definitions/fetch-video-analysis.js +0 -61
  211. package/dist/tools/extract-frames-from-video/index.d.ts +0 -7
  212. package/dist/tools/extract-frames-from-video/index.d.ts.map +0 -1
  213. package/dist/tools/extract-frames-from-video/index.js +0 -145
  214. package/dist/tools/fetch-video-analysis/index.d.ts +0 -5
  215. package/dist/tools/fetch-video-analysis/index.d.ts.map +0 -1
  216. package/dist/tools/fetch-video-analysis/index.js +0 -149
  217. package/dist/tools/fetch-video-analysis/open-ai.d.ts +0 -6
  218. package/dist/tools/fetch-video-analysis/open-ai.d.ts.map +0 -1
  219. package/dist/tools/fetch-video-analysis/open-ai.js +0 -37
  220. package/dist/tools/fetch-video-analysis/utils.d.ts +0 -16
  221. package/dist/tools/fetch-video-analysis/utils.d.ts.map +0 -1
  222. package/dist/tools/fetch-video-analysis/utils.js +0 -121
  223. package/dist/tools/fetch-video-analysis/video-analysis.d.ts +0 -7
  224. package/dist/tools/fetch-video-analysis/video-analysis.d.ts.map +0 -1
  225. package/dist/tools/fetch-video-analysis/video-analysis.js +0 -70
  226. package/dist/tools/file-operations/shared/git-helper.d.ts +0 -4
  227. package/dist/tools/file-operations/shared/git-helper.d.ts.map +0 -1
  228. package/dist/tools/file-operations/shared/git-helper.js +0 -29
  229. package/dist/utils/local-ffmpeg-client.d.ts.map +0 -1
  230. package/eslint.config.mjs +0 -43
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractTextPartFromLastMessage = extractTextPartFromLastMessage;
4
+ exports.getChatStateWithFrameIdAttachmentParts = getChatStateWithFrameIdAttachmentParts;
5
+ exports.orchestrateVideoAnalysis = orchestrateVideoAnalysis;
6
+ const agent_1 = require("../agent");
7
+ const state_1 = require("../agent/chat/state");
8
+ const executor_1 = require("../agent/video-analysis/executor");
9
+ const xml_parser_1 = require("./xml-parser");
10
+ function extractTextPartFromLastMessage(messages) {
11
+ const lastMessage = messages.length
12
+ ? messages[messages.length - 1]
13
+ : undefined;
14
+ if (!lastMessage) {
15
+ return null;
16
+ }
17
+ return lastMessage.parts
18
+ .filter((p) => "text" in p && !("thinking" in p && p.thinking))
19
+ .map((p) => p.text)
20
+ .join("\\n");
21
+ }
22
+ function getChatStateWithFrameIdAttachmentParts(attachments, selectedModel) {
23
+ const chatState = (0, state_1.createChatState)({
24
+ userPrompt: `Analyse the frames and give me a summary at the end.`,
25
+ attachments: [],
26
+ existingState: undefined,
27
+ selectedModel,
28
+ error: null,
29
+ });
30
+ const userMessageWithAttachment = attachments.map((attachment) => ({
31
+ text: `Frame ID: ${attachment.name.split(".")[0]}`,
32
+ attachments: [attachment],
33
+ }));
34
+ chatState.messages[0].parts = [
35
+ ...chatState.messages[0].parts,
36
+ ...userMessageWithAttachment,
37
+ ];
38
+ return chatState;
39
+ }
40
+ async function orchestrateVideoAnalysis({ selectedModel, featureFlags, workingDirectory, frameBatch, }) {
41
+ const frameIds = frameBatch
42
+ .map((attachment) => {
43
+ const filename = attachment.name || "";
44
+ // Remove .png extension if present
45
+ return filename.replace(/\.png$/, "");
46
+ })
47
+ .filter(Boolean)
48
+ .join(", ");
49
+ const allMessages = [];
50
+ const toolExecutor = new executor_1.VideoAnalysisToolExecutor({
51
+ chatSession: null,
52
+ repoPath: workingDirectory,
53
+ featureFlags,
54
+ environmentOverrides: {},
55
+ });
56
+ const chatState = getChatStateWithFrameIdAttachmentParts(frameBatch, selectedModel);
57
+ const agent = new agent_1.VideoAnalysisAgent({
58
+ featureFlags: featureFlags,
59
+ selectedModel,
60
+ chatState,
61
+ toolExecutor,
62
+ });
63
+ while (!agent.askUserForInput) {
64
+ await agent.runLoop({
65
+ reporter: async () => { },
66
+ });
67
+ }
68
+ allMessages.push(...agent.messages);
69
+ const rawAnalysis = extractTextPartFromLastMessage(allMessages);
70
+ if (!rawAnalysis) {
71
+ throw new Error("Could not extract text response from accumulated messages");
72
+ }
73
+ const { parsedXml, keyFrameImagesMap } = await (0, xml_parser_1.processAnalysisAndLoadFrames)({
74
+ rawAnalysis,
75
+ artifactsPath: workingDirectory,
76
+ });
77
+ return { analysis: rawAnalysis, allMessages, parsedXml, keyFrameImagesMap };
78
+ }
@@ -0,0 +1,24 @@
1
+ import http from "http";
2
+ export type AnalysisServerOptions = {
3
+ htmlFilePath?: string;
4
+ port?: number;
5
+ host?: string;
6
+ };
7
+ export type AnalysisData = {
8
+ unique_frames_count: number;
9
+ video_url: string;
10
+ analysis: string;
11
+ analysis_id: string;
12
+ params: any;
13
+ unique_frames: any[];
14
+ interleaved_tool_result: any;
15
+ };
16
+ export type AnalysisServerHandle = {
17
+ url: string;
18
+ port: number;
19
+ host: string;
20
+ server: http.Server;
21
+ close: () => Promise<void>;
22
+ };
23
+ export declare function startAnalysisServer(rootPath?: string): Promise<AnalysisServerHandle>;
24
+ //# sourceMappingURL=analysis-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analysis-server.d.ts","sourceRoot":"","sources":["../../src/video-core/analysis-server.ts"],"names":[],"mappings":"AACA,OAAO,IAAI,MAAM,MAAM,CAAC;AA+DxB,MAAM,MAAM,qBAAqB,GAAG;IAClC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,GAAG,CAAC;IACZ,aAAa,EAAE,GAAG,EAAE,CAAC;IACrB,uBAAuB,EAAE,GAAG,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B,CAAC;AAEF,wBAAsB,mBAAmB,CACvC,QAAQ,GAAE,MAAyB,GAClC,OAAO,CAAC,oBAAoB,CAAC,CAqa/B"}
@@ -0,0 +1,398 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.startAnalysisServer = startAnalysisServer;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const http_1 = __importDefault(require("http"));
9
+ const open_1 = __importDefault(require("open"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const url_1 = require("url");
12
+ const find_threshold_1 = require("../utils/find-threshold");
13
+ async function discoverAnalysisDirectories(rootPath) {
14
+ const analyses = [];
15
+ try {
16
+ await fs_1.default.promises.access(rootPath);
17
+ const entries = await fs_1.default.promises.readdir(rootPath, {
18
+ withFileTypes: true,
19
+ });
20
+ for (const entry of entries) {
21
+ if (entry.isDirectory()) {
22
+ const dirPath = path_1.default.join(rootPath, entry.name);
23
+ const analysisFilePath = path_1.default.join(dirPath, "analysis-result.json");
24
+ try {
25
+ await fs_1.default.promises.access(analysisFilePath);
26
+ const fileData = await fs_1.default.promises.readFile(analysisFilePath, "utf8");
27
+ const analysisData = JSON.parse(fileData);
28
+ const stat = await fs_1.default.promises.stat(analysisFilePath);
29
+ analyses.push({
30
+ id: entry.name,
31
+ name: entry.name,
32
+ path: dirPath,
33
+ data: analysisData,
34
+ modifiedTime: stat.mtime,
35
+ });
36
+ }
37
+ catch (error) {
38
+ // Skip directories without analysis-result.json
39
+ continue;
40
+ }
41
+ }
42
+ }
43
+ // Sort by modification time, newest first
44
+ analyses.sort((a, b) => b.modifiedTime.getTime() - a.modifiedTime.getTime());
45
+ }
46
+ catch (error) {
47
+ // Root path doesn't exist or is inaccessible
48
+ console.log(`Warning: Could not access root path ${rootPath}`);
49
+ }
50
+ return analyses;
51
+ }
52
+ async function startAnalysisServer(rootPath = "video-analysis") {
53
+ const host = "0.0.0.0";
54
+ const port = 63745;
55
+ const htmlFilePath = path_1.default.join(__dirname, "analysis-viewer.html");
56
+ let workingDir;
57
+ let currentAnalysisId;
58
+ await fs_1.default.promises.access(htmlFilePath, fs_1.default.constants.R_OK);
59
+ const server = http_1.default.createServer(async (req, res) => {
60
+ res.setHeader("Access-Control-Allow-Origin", "*");
61
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
62
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
63
+ if (req.method === "OPTIONS") {
64
+ res.statusCode = 200;
65
+ res.end();
66
+ return;
67
+ }
68
+ const url = new url_1.URL(req.url || "/", `http://${req.headers.host}`);
69
+ // API endpoint to list all available analyses
70
+ if (url.pathname === "/api/analyses" && req.method === "GET") {
71
+ const analyses = await discoverAnalysisDirectories(rootPath);
72
+ const analysesInfo = analyses.map((analysis) => ({
73
+ id: analysis.id,
74
+ name: analysis.name,
75
+ modifiedTime: analysis.modifiedTime.toISOString(),
76
+ video_url: analysis.data.video_url,
77
+ analysis_id: analysis.data.analysis_id,
78
+ unique_frames_count: analysis.data.unique_frames_count,
79
+ }));
80
+ res.statusCode = 200;
81
+ res.setHeader("Content-Type", "application/json");
82
+ res.end(JSON.stringify(analysesInfo));
83
+ return;
84
+ }
85
+ // API endpoint to get specific analysis data
86
+ if (url.pathname === "/api/data" && req.method === "GET") {
87
+ const analysisId = url.searchParams.get("id");
88
+ if (analysisId) {
89
+ const analyses = await discoverAnalysisDirectories(rootPath);
90
+ const analysis = analyses.find((a) => a.id === analysisId);
91
+ if (analysis) {
92
+ workingDir = analysis.path;
93
+ currentAnalysisId = analysisId;
94
+ res.statusCode = 200;
95
+ res.setHeader("Content-Type", "application/json");
96
+ res.end(JSON.stringify(analysis.data));
97
+ return;
98
+ }
99
+ else {
100
+ res.statusCode = 404;
101
+ res.end("Analysis not found");
102
+ return;
103
+ }
104
+ }
105
+ else if (currentAnalysisId) {
106
+ // Return current analysis data if no ID specified but one is already loaded
107
+ const analyses = await discoverAnalysisDirectories(rootPath);
108
+ const analysis = analyses.find((a) => a.id === currentAnalysisId);
109
+ if (analysis) {
110
+ res.statusCode = 200;
111
+ res.setHeader("Content-Type", "application/json");
112
+ res.end(JSON.stringify(analysis.data));
113
+ return;
114
+ }
115
+ }
116
+ res.statusCode = 404;
117
+ res.end("No analysis specified or found");
118
+ return;
119
+ }
120
+ if (url.pathname === "/api/unique-frames" && req.method === "GET") {
121
+ res.statusCode = 200;
122
+ res.setHeader("Content-Type", "application/json");
123
+ if (!workingDir || !currentAnalysisId) {
124
+ res.statusCode = 404;
125
+ res.end("No analysis loaded or working directory not found");
126
+ return;
127
+ }
128
+ // Get current analysis data
129
+ const analyses = await discoverAnalysisDirectories(rootPath);
130
+ const analysis = analyses.find((a) => a.id === currentAnalysisId);
131
+ if (!analysis) {
132
+ res.statusCode = 404;
133
+ res.end("Current analysis not found");
134
+ return;
135
+ }
136
+ const uniqueFramesDir = path_1.default.join(workingDir, "unique_frames");
137
+ console.log(" Looking for frames in:", uniqueFramesDir);
138
+ // Get FPS from analysis data, default to 30 if not available
139
+ const fps = analysis.data?.params?.fps || 30;
140
+ console.log(" Using FPS:", fps);
141
+ try {
142
+ const frameFiles = fs_1.default
143
+ .readdirSync(uniqueFramesDir)
144
+ .filter((f) => f.endsWith(".png"))
145
+ .sort();
146
+ console.log(` Found ${frameFiles.length} PNG files`);
147
+ if (frameFiles.length > 0) {
148
+ console.log(" Sample files:", frameFiles.slice(0, 3));
149
+ }
150
+ // Use async file operations for better performance
151
+ const frameDataPromises = frameFiles.map(async (filename, index) => {
152
+ if (!filename)
153
+ return null;
154
+ const frameNumber = filename.match(/frame_(\d+)/)?.[1] || String(index);
155
+ const framePath = path_1.default.join(uniqueFramesDir, filename);
156
+ try {
157
+ const stat = await fs_1.default.promises.stat(framePath);
158
+ return {
159
+ index: parseInt(frameNumber),
160
+ path: filename,
161
+ fileName: filename,
162
+ url: `/api/frame/${encodeURIComponent(filename)}`,
163
+ timestamp: `${Math.floor(parseInt(frameNumber) / fps / 60)}m${Math.floor((parseInt(frameNumber) / fps) % 60)
164
+ .toString()
165
+ .padStart(2, "0")}s`,
166
+ size: stat.size,
167
+ similarityPercentage: null, // Will be calculated on-demand
168
+ };
169
+ }
170
+ catch (error) {
171
+ console.log(` Error reading stats for ${filename}:`, error instanceof Error ? error.message : "Unknown error");
172
+ return null;
173
+ }
174
+ });
175
+ const frameDataResults = await Promise.all(frameDataPromises);
176
+ const frameData = frameDataResults.filter((frame) => frame !== null);
177
+ res.end(JSON.stringify(frameData));
178
+ return;
179
+ }
180
+ catch (error) {
181
+ console.log(" Error reading directory:", error instanceof Error ? error.message : "Unknown error");
182
+ res.statusCode = 404;
183
+ res.end(`Error reading frames directory: ${error instanceof Error ? error.message : "Unknown error"}`);
184
+ return;
185
+ }
186
+ }
187
+ if (url.pathname === "/api/similarity" && req.method === "GET") {
188
+ if (!workingDir) {
189
+ res.statusCode = 404;
190
+ res.end("Working directory not found");
191
+ return;
192
+ }
193
+ const frame1 = url.searchParams.get("frame1");
194
+ const frame2 = url.searchParams.get("frame2");
195
+ if (!frame1 || !frame2) {
196
+ res.statusCode = 400;
197
+ res.end("Missing frame1 or frame2 parameter");
198
+ return;
199
+ }
200
+ const uniqueFramesDir = path_1.default.join(workingDir, "unique_frames");
201
+ const frame1Path = path_1.default.join(uniqueFramesDir, frame1);
202
+ const frame2Path = path_1.default.join(uniqueFramesDir, frame2);
203
+ try {
204
+ // Security checks
205
+ if (frame1.includes("..") ||
206
+ frame1.includes("/") ||
207
+ frame1.includes("\\") ||
208
+ frame2.includes("..") ||
209
+ frame2.includes("/") ||
210
+ frame2.includes("\\")) {
211
+ res.statusCode = 400;
212
+ res.end("Invalid filename");
213
+ return;
214
+ }
215
+ if (!frame1.endsWith(".png") || !frame2.endsWith(".png")) {
216
+ res.statusCode = 400;
217
+ res.end("Only PNG files are supported");
218
+ return;
219
+ }
220
+ const [base64Image1, base64Image2] = await Promise.all([
221
+ fs_1.default.promises
222
+ .readFile(frame1Path)
223
+ .then((buf) => buf.toString("base64")),
224
+ fs_1.default.promises
225
+ .readFile(frame2Path)
226
+ .then((buf) => buf.toString("base64")),
227
+ ]);
228
+ const similarityPercentage = await (0, find_threshold_1.findSimilarityPercentage)(base64Image1, base64Image2);
229
+ res.statusCode = 200;
230
+ res.setHeader("Content-Type", "application/json");
231
+ res.end(JSON.stringify({ similarity: similarityPercentage }));
232
+ return;
233
+ }
234
+ catch (error) {
235
+ console.log(` Error calculating similarity for ${frame1} vs ${frame2}:`, error instanceof Error ? error.message : "Unknown error");
236
+ res.statusCode = 500;
237
+ res.end("Error calculating similarity");
238
+ return;
239
+ }
240
+ }
241
+ if (url.pathname.startsWith("/api/frame/") && req.method === "GET") {
242
+ if (!workingDir) {
243
+ res.statusCode = 404;
244
+ res.end("Working directory not found");
245
+ return;
246
+ }
247
+ const filename = decodeURIComponent(url.pathname.substring("/api/frame/".length));
248
+ const uniqueFramesDir = path_1.default.join(workingDir, "unique_frames");
249
+ const framePath = path_1.default.join(uniqueFramesDir, filename);
250
+ try {
251
+ // Security check: ensure the filename doesn't contain path traversal
252
+ if (filename.includes("..") ||
253
+ filename.includes("/") ||
254
+ filename.includes("\\")) {
255
+ res.statusCode = 400;
256
+ res.end("Invalid filename");
257
+ return;
258
+ }
259
+ // Check if file exists and is a PNG
260
+ if (!filename.endsWith(".png")) {
261
+ res.statusCode = 400;
262
+ res.end("Only PNG files are supported");
263
+ return;
264
+ }
265
+ await fs_1.default.promises.access(framePath, fs_1.default.constants.R_OK);
266
+ const imageBuffer = await fs_1.default.promises.readFile(framePath);
267
+ res.statusCode = 200;
268
+ res.setHeader("Content-Type", "image/png");
269
+ res.setHeader("Cache-Control", "public, max-age=3600"); // Cache for 1 hour
270
+ res.end(imageBuffer);
271
+ return;
272
+ }
273
+ catch (error) {
274
+ console.log(` Error serving frame ${filename}:`, error instanceof Error ? error.message : "Unknown error");
275
+ res.statusCode = 404;
276
+ res.end("Frame not found");
277
+ return;
278
+ }
279
+ }
280
+ if (req.method !== "GET" && req.method !== "HEAD") {
281
+ res.statusCode = 405;
282
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
283
+ res.end("Method Not Allowed");
284
+ return;
285
+ }
286
+ res.statusCode = 200;
287
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
288
+ res.setHeader("Cache-Control", "no-store");
289
+ try {
290
+ let htmlContent = await fs_1.default.promises.readFile(htmlFilePath, "utf8");
291
+ // Inject script to load analyses list and handle navigation
292
+ const injectionScript = `
293
+ <script>
294
+ // Load analyses list and set up navigation
295
+ window.addEventListener('DOMContentLoaded', async function() {
296
+ try {
297
+ const response = await fetch('/api/analyses');
298
+ const analyses = await response.json();
299
+
300
+ // Store analyses globally
301
+ window.currentAnalyses = analyses;
302
+
303
+ // Call the HTML implementation directly
304
+ if (typeof window.displayAnalysesList === 'function') {
305
+ window.displayAnalysesList(analyses);
306
+ }
307
+
308
+ // Auto-load first analysis if available
309
+ if (analyses.length > 0) {
310
+ loadAnalysis(analyses[0].id);
311
+ }
312
+ } catch (error) {
313
+ showError('Error loading analyses list: ' + error.message);
314
+ }
315
+ });
316
+
317
+ function loadAnalysis(id) {
318
+ fetch('/api/data?id=' + encodeURIComponent(id))
319
+ .then(response => response.json())
320
+ .then(data => {
321
+ if (typeof displayData === 'function') {
322
+ displayData(data);
323
+ }
324
+ if (typeof setActiveAnalysis === 'function') {
325
+ setActiveAnalysis(id);
326
+ }
327
+ })
328
+ .catch(error => {
329
+ showError('Error loading analysis: ' + error.message);
330
+ });
331
+ }
332
+
333
+ // Make loadAnalysis available globally
334
+ window.loadAnalysis = loadAnalysis;
335
+ </script>
336
+ `;
337
+ htmlContent = htmlContent.replace("</body>", injectionScript + "</body>");
338
+ if (req.method === "HEAD") {
339
+ res.end();
340
+ return;
341
+ }
342
+ res.end(htmlContent);
343
+ }
344
+ catch (err) {
345
+ res.statusCode = 500;
346
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
347
+ res.end(`Error reading HTML file: ${err instanceof Error ? err.message : "Unknown error"}`);
348
+ }
349
+ });
350
+ // Try to bind to the preferred port first, and if it's in use, fall back to any free port
351
+ await new Promise((resolve, reject) => {
352
+ const tryEphemeral = () => {
353
+ // Remove any existing error listeners to avoid multiple handlers
354
+ server.removeAllListeners("error");
355
+ server.once("error", reject);
356
+ server.listen(0, host, () => resolve());
357
+ };
358
+ server.once("error", (err) => {
359
+ const e = err;
360
+ if (e && e.code === "EADDRINUSE") {
361
+ // Port is taken; retry on an ephemeral port assigned by the OS
362
+ tryEphemeral();
363
+ }
364
+ else {
365
+ reject(err);
366
+ }
367
+ });
368
+ server.listen(port, host, () => resolve());
369
+ });
370
+ const address = server.address();
371
+ if (!address || typeof address === "string") {
372
+ await new Promise((resolve) => server.close(() => resolve()));
373
+ throw new Error("Server failed to bind to an address");
374
+ }
375
+ const resolvedUrl = `http://${address.address}:${address.port}`;
376
+ // Open the URL in the default browser
377
+ try {
378
+ await (0, open_1.default)(resolvedUrl);
379
+ }
380
+ catch (error) { }
381
+ const handle = {
382
+ url: resolvedUrl,
383
+ port: address.port,
384
+ host: address.address,
385
+ server,
386
+ close: () => new Promise((resolve, reject) => {
387
+ server.close((err) => {
388
+ if (err) {
389
+ reject(err);
390
+ }
391
+ else {
392
+ resolve();
393
+ }
394
+ });
395
+ }),
396
+ };
397
+ return handle;
398
+ }