@veolab/discoverylab 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/.claude-plugin/marketplace.json +15 -0
- package/.claude-plugin/plugin.json +12 -0
- package/.mcp.json +6 -0
- package/README.md +214 -0
- package/assets/applab-discovery.jpeg +0 -0
- package/assets/backgrounds/abstract-colorful-gradient-orange-background.jpg +0 -0
- package/assets/backgrounds/blurred-colorful-luxury-gradient-rainbow-abstract.jpg +0 -0
- package/assets/backgrounds/glowing-neon-moving-continuously-looking-bright.jpg +0 -0
- package/assets/backgrounds/glowing-neon-moving-continuously-looking-bright2.jpg +0 -0
- package/assets/backgrounds/macos-big-sur-apple-layers-fluidic-colorful-wwdc-stock-4096x2304-1455.jpg +0 -0
- package/assets/backgrounds/macos-sierra-mountain-peak-sunset-evening-stock-5k-5120x3684-3987.jpg +0 -0
- package/assets/backgrounds/macos-tahoe-26-5120x2880-22674.jpg +0 -0
- package/assets/backgrounds/macos-tahoe-26-5120x2880-22675.jpg +0 -0
- package/assets/backgrounds/view-of-the-sea-from-the-window-of-an-airplane-2024-10-21-11-25-30-utc.jpg +0 -0
- package/assets/cursor/cursor-blue.png +0 -0
- package/assets/icons/android-head_3D.png +0 -0
- package/assets/icons/apple-logo.png +0 -0
- package/assets/icons/apple-logo.svg +4 -0
- package/assets/icons/claude-ai-icon.svg +1 -0
- package/assets/icons/icons8-apple-intelligence-48.png +0 -0
- package/assets/icons/icons8-apple-intelligence-96.png +0 -0
- package/dist/chunk-7IDQLLBW.js +311 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-MN6LCZHZ.js +1320 -0
- package/dist/chunk-PBHUHSC3.js +6002 -0
- package/dist/chunk-QJXXHOV7.js +205 -0
- package/dist/chunk-SSRXIO2V.js +6822 -0
- package/dist/chunk-VY3BLXBW.js +329 -0
- package/dist/chunk-W3WJGYR6.js +354 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +120 -0
- package/dist/db-IWIL65EX.js +33 -0
- package/dist/gridCompositor-ENKLFPWR.js +409 -0
- package/dist/index.d.ts +1648 -0
- package/dist/index.js +869 -0
- package/dist/ocr-UTWC7537.js +21 -0
- package/dist/server-3FBHBA7L.js +15 -0
- package/dist/server-NM5CKDUU.js +13 -0
- package/dist/setup-27CQAX6K.js +17 -0
- package/dist/tools-75BAPCUM.js +177 -0
- package/package.json +84 -0
- package/skills/generate-assets/SKILL.md +44 -0
- package/skills/mobile-test/SKILL.md +33 -0
- package/skills/open-ui/SKILL.md +24 -0
- package/skills/quick-capture/SKILL.md +28 -0
- package/skills/task-hub/SKILL.md +44 -0
- package/skills/web-test/SKILL.md +41 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__export
|
|
3
|
+
} from "./chunk-MLKGABMK.js";
|
|
4
|
+
|
|
5
|
+
// src/db/index.ts
|
|
6
|
+
import Database from "better-sqlite3";
|
|
7
|
+
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
8
|
+
import { mkdirSync, existsSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
|
|
12
|
+
// src/db/schema.ts
|
|
13
|
+
var schema_exports = {};
|
|
14
|
+
__export(schema_exports, {
|
|
15
|
+
exportDestinations: () => exportDestinations,
|
|
16
|
+
exportRules: () => exportRules,
|
|
17
|
+
frames: () => frames,
|
|
18
|
+
projectExports: () => projectExports,
|
|
19
|
+
projects: () => projects,
|
|
20
|
+
settings: () => settings
|
|
21
|
+
});
|
|
22
|
+
import { sqliteTable, text, integer, real } from "drizzle-orm/sqlite-core";
|
|
23
|
+
var projects = sqliteTable("projects", {
|
|
24
|
+
id: text("id").primaryKey(),
|
|
25
|
+
name: text("name").notNull(),
|
|
26
|
+
// Source
|
|
27
|
+
videoPath: text("video_path"),
|
|
28
|
+
thumbnailPath: text("thumbnail_path"),
|
|
29
|
+
// Best frame or image for project cover
|
|
30
|
+
platform: text("platform"),
|
|
31
|
+
// 'ios' | 'android' | 'web'
|
|
32
|
+
// Analysis
|
|
33
|
+
aiSummary: text("ai_summary"),
|
|
34
|
+
ocrText: text("ocr_text"),
|
|
35
|
+
ocrEngine: text("ocr_engine"),
|
|
36
|
+
ocrConfidence: real("ocr_confidence"),
|
|
37
|
+
frameCount: integer("frame_count").default(0),
|
|
38
|
+
duration: real("duration"),
|
|
39
|
+
// seconds
|
|
40
|
+
// Annotations
|
|
41
|
+
manualNotes: text("manual_notes"),
|
|
42
|
+
tags: text("tags"),
|
|
43
|
+
// JSON array
|
|
44
|
+
linkedTicket: text("linked_ticket"),
|
|
45
|
+
// e.g., "ABC-123"
|
|
46
|
+
// External integrations (legacy single URLs - maintained for backwards compatibility)
|
|
47
|
+
linkedJiraUrl: text("linked_jira_url"),
|
|
48
|
+
// Full Jira issue URL
|
|
49
|
+
linkedNotionUrl: text("linked_notion_url"),
|
|
50
|
+
// Notion page URL
|
|
51
|
+
linkedFigmaUrl: text("linked_figma_url"),
|
|
52
|
+
// Figma design URL (auto-detected from Jira or manual)
|
|
53
|
+
// Task Hub - Multiple links with metadata (JSON array)
|
|
54
|
+
taskHubLinks: text("task_hub_links"),
|
|
55
|
+
// JSON: [{ id, type, url, title, status, metadata }]
|
|
56
|
+
taskRequirements: text("task_requirements"),
|
|
57
|
+
// AI-generated requirements from linked content
|
|
58
|
+
taskTestMap: text("task_test_map"),
|
|
59
|
+
// AI-generated test map from linked content
|
|
60
|
+
// Status
|
|
61
|
+
status: text("status").default("draft"),
|
|
62
|
+
// 'draft' | 'analyzed' | 'exported' | 'archived'
|
|
63
|
+
// Timestamps
|
|
64
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
65
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull()
|
|
66
|
+
});
|
|
67
|
+
var projectExports = sqliteTable("project_exports", {
|
|
68
|
+
id: text("id").primaryKey(),
|
|
69
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
70
|
+
// Destination
|
|
71
|
+
destination: text("destination").notNull(),
|
|
72
|
+
// 'notion' | 'drive' | 'jira' | 'slack' | 'local'
|
|
73
|
+
destinationUrl: text("destination_url"),
|
|
74
|
+
destinationPath: text("destination_path"),
|
|
75
|
+
// Content included
|
|
76
|
+
contentIncluded: text("content_included"),
|
|
77
|
+
// JSON: { video, keyFrames, aiSummary, manualNotes }
|
|
78
|
+
// Status
|
|
79
|
+
status: text("status").default("pending"),
|
|
80
|
+
// 'pending' | 'in_progress' | 'completed' | 'failed'
|
|
81
|
+
errorMessage: text("error_message"),
|
|
82
|
+
// Timestamps
|
|
83
|
+
exportedAt: integer("exported_at", { mode: "timestamp" }),
|
|
84
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull()
|
|
85
|
+
});
|
|
86
|
+
var frames = sqliteTable("frames", {
|
|
87
|
+
id: text("id").primaryKey(),
|
|
88
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
89
|
+
// Frame data
|
|
90
|
+
frameNumber: integer("frame_number").notNull(),
|
|
91
|
+
timestamp: real("timestamp").notNull(),
|
|
92
|
+
// seconds
|
|
93
|
+
imagePath: text("image_path").notNull(),
|
|
94
|
+
// Analysis
|
|
95
|
+
ocrText: text("ocr_text"),
|
|
96
|
+
isKeyFrame: integer("is_key_frame", { mode: "boolean" }).default(false),
|
|
97
|
+
// Timestamps
|
|
98
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull()
|
|
99
|
+
});
|
|
100
|
+
var settings = sqliteTable("settings", {
|
|
101
|
+
key: text("key").primaryKey(),
|
|
102
|
+
value: text("value").notNull(),
|
|
103
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull()
|
|
104
|
+
});
|
|
105
|
+
var exportDestinations = sqliteTable("export_destinations", {
|
|
106
|
+
id: text("id").primaryKey(),
|
|
107
|
+
// Destination config
|
|
108
|
+
type: text("type").notNull(),
|
|
109
|
+
// 'notion' | 'drive' | 'jira' | 'slack'
|
|
110
|
+
name: text("name").notNull(),
|
|
111
|
+
// User-friendly name
|
|
112
|
+
// Configuration (JSON)
|
|
113
|
+
config: text("config"),
|
|
114
|
+
// { workspaceUrl, folderId, projectKey, channelId, etc. }
|
|
115
|
+
// Status
|
|
116
|
+
isActive: integer("is_active", { mode: "boolean" }).default(true),
|
|
117
|
+
lastUsedAt: integer("last_used_at", { mode: "timestamp" }),
|
|
118
|
+
// Timestamps
|
|
119
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
120
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull()
|
|
121
|
+
});
|
|
122
|
+
var exportRules = sqliteTable("export_rules", {
|
|
123
|
+
id: text("id").primaryKey(),
|
|
124
|
+
// Rule definition
|
|
125
|
+
name: text("name").notNull(),
|
|
126
|
+
destinationId: text("destination_id").notNull().references(() => exportDestinations.id, { onDelete: "cascade" }),
|
|
127
|
+
// Trigger conditions (JSON)
|
|
128
|
+
conditions: text("conditions"),
|
|
129
|
+
// { tags: ['feature'], status: 'analyzed', platform: 'ios' }
|
|
130
|
+
// Content to include (JSON)
|
|
131
|
+
contentIncluded: text("content_included"),
|
|
132
|
+
// { video: true, keyFrames: true, aiSummary: true }
|
|
133
|
+
// Status
|
|
134
|
+
isActive: integer("is_active", { mode: "boolean" }).default(true),
|
|
135
|
+
// Timestamps
|
|
136
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
137
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull()
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// src/db/index.ts
|
|
141
|
+
var DATA_DIR = join(homedir(), ".discoverylab");
|
|
142
|
+
var DB_PATH = join(DATA_DIR, "data.db");
|
|
143
|
+
var PROJECTS_DIR = join(DATA_DIR, "projects");
|
|
144
|
+
var EXPORTS_DIR = join(DATA_DIR, "exports");
|
|
145
|
+
var FRAMES_DIR = join(DATA_DIR, "frames");
|
|
146
|
+
function ensureDirectories() {
|
|
147
|
+
const dirs = [DATA_DIR, PROJECTS_DIR, EXPORTS_DIR, FRAMES_DIR];
|
|
148
|
+
for (const dir of dirs) {
|
|
149
|
+
if (!existsSync(dir)) {
|
|
150
|
+
mkdirSync(dir, { recursive: true });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function createTables(sqlite) {
|
|
155
|
+
sqlite.exec(`
|
|
156
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
157
|
+
id TEXT PRIMARY KEY,
|
|
158
|
+
name TEXT NOT NULL,
|
|
159
|
+
video_path TEXT,
|
|
160
|
+
thumbnail_path TEXT,
|
|
161
|
+
platform TEXT,
|
|
162
|
+
ai_summary TEXT,
|
|
163
|
+
ocr_text TEXT,
|
|
164
|
+
ocr_engine TEXT,
|
|
165
|
+
ocr_confidence REAL,
|
|
166
|
+
frame_count INTEGER DEFAULT 0,
|
|
167
|
+
duration REAL,
|
|
168
|
+
manual_notes TEXT,
|
|
169
|
+
tags TEXT,
|
|
170
|
+
linked_ticket TEXT,
|
|
171
|
+
linked_jira_url TEXT,
|
|
172
|
+
linked_notion_url TEXT,
|
|
173
|
+
linked_figma_url TEXT,
|
|
174
|
+
task_hub_links TEXT,
|
|
175
|
+
task_requirements TEXT,
|
|
176
|
+
task_test_map TEXT,
|
|
177
|
+
status TEXT DEFAULT 'draft',
|
|
178
|
+
created_at INTEGER NOT NULL,
|
|
179
|
+
updated_at INTEGER NOT NULL
|
|
180
|
+
)
|
|
181
|
+
`);
|
|
182
|
+
try {
|
|
183
|
+
sqlite.exec(`ALTER TABLE projects ADD COLUMN thumbnail_path TEXT`);
|
|
184
|
+
} catch (e) {
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
sqlite.exec(`ALTER TABLE projects ADD COLUMN linked_jira_url TEXT`);
|
|
188
|
+
} catch (e) {
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
sqlite.exec(`ALTER TABLE projects ADD COLUMN linked_notion_url TEXT`);
|
|
192
|
+
} catch (e) {
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
sqlite.exec(`ALTER TABLE projects ADD COLUMN linked_figma_url TEXT`);
|
|
196
|
+
} catch (e) {
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
sqlite.exec(`ALTER TABLE projects ADD COLUMN task_hub_links TEXT`);
|
|
200
|
+
} catch (e) {
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
sqlite.exec(`ALTER TABLE projects ADD COLUMN task_requirements TEXT`);
|
|
204
|
+
} catch (e) {
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
sqlite.exec(`ALTER TABLE projects ADD COLUMN task_test_map TEXT`);
|
|
208
|
+
} catch (e) {
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
sqlite.exec(`ALTER TABLE projects ADD COLUMN ocr_engine TEXT`);
|
|
212
|
+
} catch (e) {
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
sqlite.exec(`ALTER TABLE projects ADD COLUMN ocr_confidence REAL`);
|
|
216
|
+
} catch (e) {
|
|
217
|
+
}
|
|
218
|
+
sqlite.exec(`
|
|
219
|
+
CREATE TABLE IF NOT EXISTS project_exports (
|
|
220
|
+
id TEXT PRIMARY KEY,
|
|
221
|
+
project_id TEXT NOT NULL,
|
|
222
|
+
destination TEXT NOT NULL,
|
|
223
|
+
destination_url TEXT,
|
|
224
|
+
destination_path TEXT,
|
|
225
|
+
content_included TEXT,
|
|
226
|
+
status TEXT DEFAULT 'pending',
|
|
227
|
+
error_message TEXT,
|
|
228
|
+
exported_at INTEGER,
|
|
229
|
+
created_at INTEGER NOT NULL,
|
|
230
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
231
|
+
)
|
|
232
|
+
`);
|
|
233
|
+
sqlite.exec(`
|
|
234
|
+
CREATE TABLE IF NOT EXISTS frames (
|
|
235
|
+
id TEXT PRIMARY KEY,
|
|
236
|
+
project_id TEXT NOT NULL,
|
|
237
|
+
frame_number INTEGER NOT NULL,
|
|
238
|
+
timestamp REAL NOT NULL,
|
|
239
|
+
image_path TEXT NOT NULL,
|
|
240
|
+
ocr_text TEXT,
|
|
241
|
+
is_key_frame INTEGER DEFAULT 0,
|
|
242
|
+
created_at INTEGER NOT NULL,
|
|
243
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
244
|
+
)
|
|
245
|
+
`);
|
|
246
|
+
sqlite.exec(`
|
|
247
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
248
|
+
key TEXT PRIMARY KEY,
|
|
249
|
+
value TEXT NOT NULL,
|
|
250
|
+
updated_at INTEGER NOT NULL
|
|
251
|
+
)
|
|
252
|
+
`);
|
|
253
|
+
sqlite.exec(`
|
|
254
|
+
CREATE TABLE IF NOT EXISTS export_destinations (
|
|
255
|
+
id TEXT PRIMARY KEY,
|
|
256
|
+
type TEXT NOT NULL,
|
|
257
|
+
name TEXT NOT NULL,
|
|
258
|
+
config TEXT,
|
|
259
|
+
is_active INTEGER DEFAULT 1,
|
|
260
|
+
last_used_at INTEGER,
|
|
261
|
+
created_at INTEGER NOT NULL,
|
|
262
|
+
updated_at INTEGER NOT NULL
|
|
263
|
+
)
|
|
264
|
+
`);
|
|
265
|
+
sqlite.exec(`
|
|
266
|
+
CREATE TABLE IF NOT EXISTS export_rules (
|
|
267
|
+
id TEXT PRIMARY KEY,
|
|
268
|
+
name TEXT NOT NULL,
|
|
269
|
+
destination_id TEXT NOT NULL,
|
|
270
|
+
conditions TEXT,
|
|
271
|
+
content_included TEXT,
|
|
272
|
+
is_active INTEGER DEFAULT 1,
|
|
273
|
+
created_at INTEGER NOT NULL,
|
|
274
|
+
updated_at INTEGER NOT NULL,
|
|
275
|
+
FOREIGN KEY (destination_id) REFERENCES export_destinations(id) ON DELETE CASCADE
|
|
276
|
+
)
|
|
277
|
+
`);
|
|
278
|
+
sqlite.exec(`
|
|
279
|
+
CREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status);
|
|
280
|
+
CREATE INDEX IF NOT EXISTS idx_projects_platform ON projects(platform);
|
|
281
|
+
CREATE INDEX IF NOT EXISTS idx_project_exports_project_id ON project_exports(project_id);
|
|
282
|
+
CREATE INDEX IF NOT EXISTS idx_project_exports_destination ON project_exports(destination);
|
|
283
|
+
CREATE INDEX IF NOT EXISTS idx_frames_project_id ON frames(project_id);
|
|
284
|
+
CREATE INDEX IF NOT EXISTS idx_frames_is_key_frame ON frames(is_key_frame);
|
|
285
|
+
`);
|
|
286
|
+
}
|
|
287
|
+
var _db = null;
|
|
288
|
+
var _sqlite = null;
|
|
289
|
+
function getDatabase() {
|
|
290
|
+
if (!_db) {
|
|
291
|
+
ensureDirectories();
|
|
292
|
+
_sqlite = new Database(DB_PATH);
|
|
293
|
+
_sqlite.pragma("journal_mode = WAL");
|
|
294
|
+
_sqlite.pragma("foreign_keys = ON");
|
|
295
|
+
createTables(_sqlite);
|
|
296
|
+
_db = drizzle(_sqlite, { schema: schema_exports });
|
|
297
|
+
}
|
|
298
|
+
return _db;
|
|
299
|
+
}
|
|
300
|
+
function getSqlite() {
|
|
301
|
+
if (!_sqlite) {
|
|
302
|
+
getDatabase();
|
|
303
|
+
}
|
|
304
|
+
return _sqlite;
|
|
305
|
+
}
|
|
306
|
+
function closeDatabase() {
|
|
307
|
+
if (_sqlite) {
|
|
308
|
+
_sqlite.close();
|
|
309
|
+
_sqlite = null;
|
|
310
|
+
_db = null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export {
|
|
315
|
+
projects,
|
|
316
|
+
projectExports,
|
|
317
|
+
frames,
|
|
318
|
+
settings,
|
|
319
|
+
exportDestinations,
|
|
320
|
+
exportRules,
|
|
321
|
+
DATA_DIR,
|
|
322
|
+
DB_PATH,
|
|
323
|
+
PROJECTS_DIR,
|
|
324
|
+
EXPORTS_DIR,
|
|
325
|
+
FRAMES_DIR,
|
|
326
|
+
getDatabase,
|
|
327
|
+
getSqlite,
|
|
328
|
+
closeDatabase
|
|
329
|
+
};
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
// src/core/analyze/ocr.ts
|
|
2
|
+
import { execSync, spawn } from "child_process";
|
|
3
|
+
import { existsSync, writeFileSync, unlinkSync } from "fs";
|
|
4
|
+
import { join, dirname } from "path";
|
|
5
|
+
import { platform, tmpdir } from "os";
|
|
6
|
+
import { randomUUID } from "crypto";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
var __dirname = dirname(__filename);
|
|
10
|
+
function getSwiftHelperPath() {
|
|
11
|
+
const possiblePaths = [
|
|
12
|
+
join(__dirname, "..", "..", "swift", "VisionHelper"),
|
|
13
|
+
join(__dirname, "..", "..", "..", "src", "swift", "VisionHelper"),
|
|
14
|
+
join(process.cwd(), "src", "swift", "VisionHelper")
|
|
15
|
+
];
|
|
16
|
+
for (const path of possiblePaths) {
|
|
17
|
+
if (existsSync(path)) {
|
|
18
|
+
return path;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return possiblePaths[0];
|
|
22
|
+
}
|
|
23
|
+
async function recognizeTextWithVision(imagePath, options = {}) {
|
|
24
|
+
if (platform() !== "darwin") {
|
|
25
|
+
return { success: false, error: "Apple Vision is only available on macOS", engine: "vision" };
|
|
26
|
+
}
|
|
27
|
+
if (!existsSync(imagePath)) {
|
|
28
|
+
return { success: false, error: `Image file not found: ${imagePath}`, engine: "vision" };
|
|
29
|
+
}
|
|
30
|
+
const helperPath = getSwiftHelperPath();
|
|
31
|
+
if (existsSync(helperPath)) {
|
|
32
|
+
return runSwiftHelper(helperPath, imagePath, options);
|
|
33
|
+
}
|
|
34
|
+
return runVisionViaAppleScript(imagePath, options);
|
|
35
|
+
}
|
|
36
|
+
async function runSwiftHelper(helperPath, imagePath, options) {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
const args = ["ocr", imagePath];
|
|
39
|
+
if (options.recognitionLevel === "fast") {
|
|
40
|
+
args.push("--fast");
|
|
41
|
+
}
|
|
42
|
+
if (options.languages?.length) {
|
|
43
|
+
args.push("--languages", options.languages.join(","));
|
|
44
|
+
}
|
|
45
|
+
const proc = spawn(helperPath, args);
|
|
46
|
+
let stdout = "";
|
|
47
|
+
let stderr = "";
|
|
48
|
+
proc.stdout.on("data", (data) => {
|
|
49
|
+
stdout += data.toString();
|
|
50
|
+
});
|
|
51
|
+
proc.stderr.on("data", (data) => {
|
|
52
|
+
stderr += data.toString();
|
|
53
|
+
});
|
|
54
|
+
proc.on("close", (code) => {
|
|
55
|
+
if (code !== 0) {
|
|
56
|
+
resolve({ success: false, error: stderr || "VisionHelper failed", engine: "vision" });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const result = JSON.parse(stdout);
|
|
61
|
+
resolve({
|
|
62
|
+
success: true,
|
|
63
|
+
text: result.text,
|
|
64
|
+
blocks: result.blocks,
|
|
65
|
+
confidence: result.confidence,
|
|
66
|
+
engine: "vision"
|
|
67
|
+
});
|
|
68
|
+
} catch {
|
|
69
|
+
resolve({
|
|
70
|
+
success: true,
|
|
71
|
+
text: stdout.trim(),
|
|
72
|
+
engine: "vision"
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
proc.on("error", (err) => {
|
|
77
|
+
resolve({ success: false, error: err.message, engine: "vision" });
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async function runVisionViaAppleScript(imagePath, _options) {
|
|
82
|
+
const swiftCode = `
|
|
83
|
+
import Cocoa
|
|
84
|
+
import Vision
|
|
85
|
+
|
|
86
|
+
let imagePath = "${imagePath.replace(/"/g, '\\"')}"
|
|
87
|
+
guard let image = NSImage(contentsOfFile: imagePath),
|
|
88
|
+
let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
|
|
89
|
+
print("Error: Could not load image")
|
|
90
|
+
exit(1)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let request = VNRecognizeTextRequest()
|
|
94
|
+
request.recognitionLevel = .accurate
|
|
95
|
+
request.recognitionLanguages = ["en-US"]
|
|
96
|
+
|
|
97
|
+
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
|
|
98
|
+
try? handler.perform([request])
|
|
99
|
+
|
|
100
|
+
var allText = ""
|
|
101
|
+
if let results = request.results {
|
|
102
|
+
for observation in results {
|
|
103
|
+
if let candidate = observation.topCandidates(1).first {
|
|
104
|
+
allText += candidate.string + "\\n"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
print(allText)
|
|
110
|
+
`;
|
|
111
|
+
const tempFile = join(tmpdir(), `vision-ocr-${randomUUID()}.swift`);
|
|
112
|
+
try {
|
|
113
|
+
writeFileSync(tempFile, swiftCode);
|
|
114
|
+
const output = execSync(`swift ${tempFile}`, {
|
|
115
|
+
encoding: "utf-8",
|
|
116
|
+
timeout: 3e4
|
|
117
|
+
});
|
|
118
|
+
unlinkSync(tempFile);
|
|
119
|
+
return {
|
|
120
|
+
success: true,
|
|
121
|
+
text: output.trim(),
|
|
122
|
+
engine: "vision"
|
|
123
|
+
};
|
|
124
|
+
} catch (error) {
|
|
125
|
+
try {
|
|
126
|
+
unlinkSync(tempFile);
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
const message = error instanceof Error ? error.message : "Vision OCR failed";
|
|
130
|
+
return { success: false, error: message, engine: "vision" };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function recognizeTextWithTesseract(imagePath, options = {}) {
|
|
134
|
+
if (!existsSync(imagePath)) {
|
|
135
|
+
return { success: false, error: `Image file not found: ${imagePath}`, engine: "tesseract" };
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
execSync("tesseract --version", { stdio: "ignore" });
|
|
139
|
+
} catch {
|
|
140
|
+
return { success: false, error: "Tesseract is not installed", engine: "tesseract" };
|
|
141
|
+
}
|
|
142
|
+
return new Promise((resolve) => {
|
|
143
|
+
const args = [imagePath, "stdout"];
|
|
144
|
+
if (options.languages?.length) {
|
|
145
|
+
args.push("-l", options.languages.join("+"));
|
|
146
|
+
}
|
|
147
|
+
args.push("--psm", "3");
|
|
148
|
+
args.push("-c", "tessedit_create_hocr=0");
|
|
149
|
+
const proc = spawn("tesseract", args);
|
|
150
|
+
let stdout = "";
|
|
151
|
+
let stderr = "";
|
|
152
|
+
proc.stdout.on("data", (data) => {
|
|
153
|
+
stdout += data.toString();
|
|
154
|
+
});
|
|
155
|
+
proc.stderr.on("data", (data) => {
|
|
156
|
+
stderr += data.toString();
|
|
157
|
+
});
|
|
158
|
+
proc.on("close", (code) => {
|
|
159
|
+
if (code !== 0 && !stdout) {
|
|
160
|
+
resolve({ success: false, error: stderr || "Tesseract failed", engine: "tesseract" });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
resolve({
|
|
164
|
+
success: true,
|
|
165
|
+
text: stdout.trim(),
|
|
166
|
+
engine: "tesseract"
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
proc.on("error", (err) => {
|
|
170
|
+
resolve({ success: false, error: err.message, engine: "tesseract" });
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async function recognizeText(imagePath, options = {}) {
|
|
175
|
+
if (platform() === "darwin") {
|
|
176
|
+
const visionResult = await recognizeTextWithVision(imagePath, options);
|
|
177
|
+
if (visionResult.success) {
|
|
178
|
+
return visionResult;
|
|
179
|
+
}
|
|
180
|
+
console.warn("Vision OCR failed, trying Tesseract:", visionResult.error);
|
|
181
|
+
}
|
|
182
|
+
return recognizeTextWithTesseract(imagePath, options);
|
|
183
|
+
}
|
|
184
|
+
async function recognizeTextBatch(imagePaths, options = {}) {
|
|
185
|
+
const results = [];
|
|
186
|
+
const textParts = [];
|
|
187
|
+
for (const imagePath of imagePaths) {
|
|
188
|
+
const ocr = await recognizeText(imagePath, options);
|
|
189
|
+
results.push({ imagePath, ocr });
|
|
190
|
+
if (ocr.success && ocr.text) {
|
|
191
|
+
textParts.push(ocr.text);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
success: results.some((r) => r.ocr.success),
|
|
196
|
+
results,
|
|
197
|
+
totalText: textParts.join("\n\n---\n\n")
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function analyzeText(text) {
|
|
201
|
+
if (!text || text.trim().length === 0) {
|
|
202
|
+
return {
|
|
203
|
+
wordCount: 0,
|
|
204
|
+
uniqueWords: 0,
|
|
205
|
+
sentences: [],
|
|
206
|
+
keywords: []
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const words = text.toLowerCase().split(/\s+/).filter((w) => w.length > 0).map((w) => w.replace(/[^\w]/g, ""));
|
|
210
|
+
const uniqueWords = new Set(words);
|
|
211
|
+
const sentences = text.split(/[.!?]+/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
212
|
+
const wordFreq = /* @__PURE__ */ new Map();
|
|
213
|
+
const stopWords = /* @__PURE__ */ new Set([
|
|
214
|
+
"the",
|
|
215
|
+
"a",
|
|
216
|
+
"an",
|
|
217
|
+
"is",
|
|
218
|
+
"are",
|
|
219
|
+
"was",
|
|
220
|
+
"were",
|
|
221
|
+
"be",
|
|
222
|
+
"been",
|
|
223
|
+
"being",
|
|
224
|
+
"have",
|
|
225
|
+
"has",
|
|
226
|
+
"had",
|
|
227
|
+
"do",
|
|
228
|
+
"does",
|
|
229
|
+
"did",
|
|
230
|
+
"will",
|
|
231
|
+
"would",
|
|
232
|
+
"could",
|
|
233
|
+
"should",
|
|
234
|
+
"may",
|
|
235
|
+
"might",
|
|
236
|
+
"must",
|
|
237
|
+
"shall",
|
|
238
|
+
"can",
|
|
239
|
+
"need",
|
|
240
|
+
"dare",
|
|
241
|
+
"to",
|
|
242
|
+
"of",
|
|
243
|
+
"in",
|
|
244
|
+
"for",
|
|
245
|
+
"on",
|
|
246
|
+
"with",
|
|
247
|
+
"at",
|
|
248
|
+
"by",
|
|
249
|
+
"from",
|
|
250
|
+
"up",
|
|
251
|
+
"about",
|
|
252
|
+
"into",
|
|
253
|
+
"through",
|
|
254
|
+
"during",
|
|
255
|
+
"before",
|
|
256
|
+
"after",
|
|
257
|
+
"above",
|
|
258
|
+
"below",
|
|
259
|
+
"between",
|
|
260
|
+
"under",
|
|
261
|
+
"again",
|
|
262
|
+
"further",
|
|
263
|
+
"then",
|
|
264
|
+
"once",
|
|
265
|
+
"and",
|
|
266
|
+
"but",
|
|
267
|
+
"or",
|
|
268
|
+
"nor",
|
|
269
|
+
"so",
|
|
270
|
+
"yet",
|
|
271
|
+
"both",
|
|
272
|
+
"either",
|
|
273
|
+
"neither",
|
|
274
|
+
"not",
|
|
275
|
+
"only",
|
|
276
|
+
"own",
|
|
277
|
+
"same",
|
|
278
|
+
"than",
|
|
279
|
+
"too",
|
|
280
|
+
"very",
|
|
281
|
+
"just",
|
|
282
|
+
"it",
|
|
283
|
+
"its",
|
|
284
|
+
"this",
|
|
285
|
+
"that",
|
|
286
|
+
"these",
|
|
287
|
+
"those",
|
|
288
|
+
"i",
|
|
289
|
+
"you",
|
|
290
|
+
"he",
|
|
291
|
+
"she",
|
|
292
|
+
"we",
|
|
293
|
+
"they",
|
|
294
|
+
"me",
|
|
295
|
+
"him",
|
|
296
|
+
"her",
|
|
297
|
+
"us",
|
|
298
|
+
"them",
|
|
299
|
+
"my",
|
|
300
|
+
"your"
|
|
301
|
+
]);
|
|
302
|
+
for (const word of words) {
|
|
303
|
+
if (word.length > 2 && !stopWords.has(word)) {
|
|
304
|
+
wordFreq.set(word, (wordFreq.get(word) || 0) + 1);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const keywords = Array.from(wordFreq.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([word]) => word);
|
|
308
|
+
return {
|
|
309
|
+
wordCount: words.length,
|
|
310
|
+
uniqueWords: uniqueWords.size,
|
|
311
|
+
sentences,
|
|
312
|
+
keywords
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function checkVisionAvailable() {
|
|
316
|
+
if (platform() !== "darwin") {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
execSync('swift -e "import Vision"', { stdio: "ignore", timeout: 5e3 });
|
|
321
|
+
return true;
|
|
322
|
+
} catch {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function checkTesseractAvailable() {
|
|
327
|
+
try {
|
|
328
|
+
execSync("tesseract --version", { stdio: "ignore" });
|
|
329
|
+
return true;
|
|
330
|
+
} catch {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function getAvailableOCREngines() {
|
|
335
|
+
const engines = [];
|
|
336
|
+
if (checkVisionAvailable()) {
|
|
337
|
+
engines.push("vision");
|
|
338
|
+
}
|
|
339
|
+
if (checkTesseractAvailable()) {
|
|
340
|
+
engines.push("tesseract");
|
|
341
|
+
}
|
|
342
|
+
return engines;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export {
|
|
346
|
+
recognizeTextWithVision,
|
|
347
|
+
recognizeTextWithTesseract,
|
|
348
|
+
recognizeText,
|
|
349
|
+
recognizeTextBatch,
|
|
350
|
+
analyzeText,
|
|
351
|
+
checkVisionAvailable,
|
|
352
|
+
checkTesseractAvailable,
|
|
353
|
+
getAvailableOCREngines
|
|
354
|
+
};
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|