@openspecui/server 3.9.0 → 3.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +230 -8
- package/dist/src-DBFY1eQK.mjs +121 -0
- package/dist/src-DuQ_-3Kn.mjs +13440 -0
- package/package.json +4 -6
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { BatchTranslateInputSchema, CliExecutor, CodeEditorThemeSchema, ConfigManager, CustomSoundHashSchema, DASHBOARD_METRIC_KEYS, DashboardConfigSchema, DocumentTranslationConfigSchema, GitConfigSchema, GlobalSettingsManager, LocalModelAssetStateSchema, MarkdownParser, NotificationPublishInputSchema, NotificationSettingsSchema, OPENSPECUI_HOOKS_VERSION, OpenSpecAdapter, OpenSpecUIGlobalSettingsSchema, OpenSpecWatcher, OpsxConfigSchema, OpsxKernel, PtyClientMessageSchema, ReactiveContext, ServiceTranslationEngineIdSchema, TRANSLATION_ENGINE_MANIFESTS, TerminalConfigSchema, TerminalControlParser, TerminalRendererEngineSchema, TranslationCacheReadInputSchema, TranslationCacheSettingsSchema, TranslationCacheWriteInputSchema, TranslationEngineIdSchema, TranslationLocalSettingsSchema, TranslationOpenAISettingsSchema, buildBackendHealthPayload, buildLocalDownloadPlanFromRepositoryFiles, clearCache, getAllTools, getAvailableTools, getConfiguredTools, getDefaultCliCommandString, getDetectedProjectTools, getToolInitStates, getWatcherRuntimeStatus, initWatcherPool, isWatcherPoolInitialized, parseOpsxEntityMetadata, parseOpsxSchemaDetail, resolveTerminalShellDefaults, selectLocalDownloadGroup, sniffGlobalCli, subscribeWatcherRuntimeStatus, terminalNotificationEventToPublishInput } from "@openspecui/core";
|
|
2
|
-
import { basename, dirname, join, matchesGlob, relative, resolve, sep } from "node:path";
|
|
1
|
+
import { BatchTranslateInputSchema, CliExecutor, CodeEditorThemeSchema, ConfigManager, CustomSoundHashSchema, DASHBOARD_METRIC_KEYS, DashboardConfigSchema, DocumentTranslationConfigSchema, GitConfigSchema, GlobalSettingsManager, LocalModelAssetStateSchema, MarkdownParser, NotificationPublishInputSchema, NotificationSettingsSchema, OPENSPECUI_HOOKS_VERSION, OpenSpecAdapter, OpenSpecUIGlobalSettingsSchema, OpenSpecWatcher, OpsxConfigSchema, OpsxKernel, PtyClientMessageSchema, ReactiveContext, ServiceTranslationEngineIdSchema, TRANSLATION_ENGINE_MANIFESTS, TerminalConfigSchema, TerminalControlParser, TerminalRendererEngineSchema, TranslationCacheReadInputSchema, TranslationCacheSettingsSchema, TranslationCacheWriteInputSchema, TranslationEngineIdSchema, TranslationLocalSettingsSchema, TranslationOpenAISettingsSchema, buildBackendHealthPayload, buildLocalDownloadPlanFromRepositoryFiles, clearCache, getAllTools, getAvailableTools, getConfiguredTools, getDefaultCliCommandString, getDetectedProjectTools, getOpsxEntityRootRelativePath, getToolInitStates, getWatcherRuntimeStatus, inferFileMime, inferFilePreviewKind, initWatcherPool, isWatcherPoolInitialized, normalizeOpsxEntityPath, parseOpsxEntityMetadata, parseOpsxSchemaDetail, resolveTerminalShellDefaults, selectLocalDownloadGroup, sniffGlobalCli, subscribeWatcherRuntimeStatus, terminalNotificationEventToPublishInput } from "@openspecui/core";
|
|
2
|
+
import { basename, dirname, extname, join, matchesGlob, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { access, copyFile, lstat, mkdir, open, readFile, readlink, realpath, rename, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import { createServer as createServer$1 } from "node:net";
|
|
@@ -8,7 +8,7 @@ import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
|
|
8
8
|
import { applyWSSHandler } from "@trpc/server/adapters/ws";
|
|
9
9
|
import { Hono } from "hono";
|
|
10
10
|
import { cors } from "hono/cors";
|
|
11
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
12
12
|
import { WebSocketServer } from "ws";
|
|
13
13
|
import { CustomSoundHashSchema as CustomSoundHashSchema$1, CustomSoundIdSchema, CustomSoundMetadataFileSchema, customHashFromSoundId, soundIdFromCustomHash } from "@openspecui/core/sounds";
|
|
14
14
|
import { createHash } from "node:crypto";
|
|
@@ -1450,6 +1450,160 @@ async function buildEntityReadOptions(ctx, stage, id) {
|
|
|
1450
1450
|
}
|
|
1451
1451
|
}
|
|
1452
1452
|
|
|
1453
|
+
//#endregion
|
|
1454
|
+
//#region src/entity-file-paths.ts
|
|
1455
|
+
function ensureInsideRoot(rootPath, candidatePath) {
|
|
1456
|
+
if (candidatePath === rootPath) return;
|
|
1457
|
+
if (!candidatePath.startsWith(rootPath + "/")) throw new Error("Resolved path escaped entity root.");
|
|
1458
|
+
}
|
|
1459
|
+
function getEntityRootPath(projectDir, stage, changeId) {
|
|
1460
|
+
return resolve(projectDir, getOpsxEntityRootRelativePath(stage, changeId));
|
|
1461
|
+
}
|
|
1462
|
+
function resolveEntityEntryPath(input) {
|
|
1463
|
+
const relativePath$1 = normalizeOpsxEntityPath(input.path);
|
|
1464
|
+
if (!relativePath$1) throw new Error("path is required");
|
|
1465
|
+
const entityRoot = getEntityRootPath(input.projectDir, input.stage, input.changeId);
|
|
1466
|
+
const absolutePath = resolve(entityRoot, relativePath$1);
|
|
1467
|
+
ensureInsideRoot(entityRoot, absolutePath);
|
|
1468
|
+
return {
|
|
1469
|
+
entityRoot,
|
|
1470
|
+
relativePath: relativePath$1,
|
|
1471
|
+
absolutePath
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
//#endregion
|
|
1476
|
+
//#region src/file-preview-service.ts
|
|
1477
|
+
const PREVIEW_ENTRY_FILE_BY_KIND = {
|
|
1478
|
+
image: "image-preview.html",
|
|
1479
|
+
audio: "audio-preview.html",
|
|
1480
|
+
video: "video-preview.html",
|
|
1481
|
+
pdf: "pdf-preview.html"
|
|
1482
|
+
};
|
|
1483
|
+
const SESSION_PREVIEW_KINDS = new Set([
|
|
1484
|
+
"html",
|
|
1485
|
+
"image",
|
|
1486
|
+
"audio",
|
|
1487
|
+
"video",
|
|
1488
|
+
"pdf"
|
|
1489
|
+
]);
|
|
1490
|
+
function isSessionPreviewKind(previewKind) {
|
|
1491
|
+
return SESSION_PREVIEW_KINDS.has(previewKind);
|
|
1492
|
+
}
|
|
1493
|
+
function toHash(input) {
|
|
1494
|
+
return createHash("sha256").update(input).digest("hex");
|
|
1495
|
+
}
|
|
1496
|
+
function stripLeadingSlash(path) {
|
|
1497
|
+
return path.replace(/^\/+/, "");
|
|
1498
|
+
}
|
|
1499
|
+
function inferPreviewAssetContentType(path) {
|
|
1500
|
+
switch (extname(path).toLowerCase()) {
|
|
1501
|
+
case ".html": return "text/html";
|
|
1502
|
+
case ".js":
|
|
1503
|
+
case ".mjs": return "application/javascript";
|
|
1504
|
+
case ".css": return "text/css";
|
|
1505
|
+
case ".json": return "application/json";
|
|
1506
|
+
case ".svg": return "image/svg+xml";
|
|
1507
|
+
case ".png": return "image/png";
|
|
1508
|
+
case ".jpg":
|
|
1509
|
+
case ".jpeg": return "image/jpeg";
|
|
1510
|
+
case ".woff": return "font/woff";
|
|
1511
|
+
case ".woff2": return "font/woff2";
|
|
1512
|
+
default: return inferFileMime(path) ?? "application/octet-stream";
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
function isRewritablePreviewAsset(path) {
|
|
1516
|
+
const extension = extname(path).toLowerCase();
|
|
1517
|
+
return extension === ".html" || extension === ".js" || extension === ".mjs" || extension === ".css";
|
|
1518
|
+
}
|
|
1519
|
+
function rewritePreviewAssetPaths(content, hash) {
|
|
1520
|
+
const sessionAssetPrefix = `/api/file-preview/${hash}/assets/`;
|
|
1521
|
+
return content.replaceAll("/assets/", sessionAssetPrefix);
|
|
1522
|
+
}
|
|
1523
|
+
var FilePreviewService = class {
|
|
1524
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1525
|
+
constructor(projectDir, previewAssetsDir) {
|
|
1526
|
+
this.projectDir = projectDir;
|
|
1527
|
+
this.previewAssetsDir = previewAssetsDir;
|
|
1528
|
+
}
|
|
1529
|
+
prepareEntityFilePreview(input) {
|
|
1530
|
+
const resolved = resolveEntityEntryPath({
|
|
1531
|
+
projectDir: this.projectDir,
|
|
1532
|
+
stage: input.stage,
|
|
1533
|
+
changeId: input.changeId,
|
|
1534
|
+
path: input.path
|
|
1535
|
+
});
|
|
1536
|
+
if (!statSync(resolved.absolutePath, { throwIfNoEntry: false })?.isFile()) throw new Error("Preview target file not found.");
|
|
1537
|
+
const mime = inferFileMime(resolved.relativePath);
|
|
1538
|
+
if (!mime) throw new Error("Preview target mime is unknown.");
|
|
1539
|
+
const previewKind = inferFilePreviewKind(resolved.relativePath, mime);
|
|
1540
|
+
if (!isSessionPreviewKind(previewKind)) throw new Error("Preview route is not supported for this file type.");
|
|
1541
|
+
const directoryPath = resolve(resolved.absolutePath, "..");
|
|
1542
|
+
const hash = toHash(`${directoryPath}:${mime}`);
|
|
1543
|
+
const entryFileName = previewKind === "html" ? null : PREVIEW_ENTRY_FILE_BY_KIND[previewKind];
|
|
1544
|
+
const fileName = basename(resolved.absolutePath);
|
|
1545
|
+
this.sessions.set(hash, {
|
|
1546
|
+
hash,
|
|
1547
|
+
directoryPath,
|
|
1548
|
+
mime,
|
|
1549
|
+
previewKind,
|
|
1550
|
+
entryFileName
|
|
1551
|
+
});
|
|
1552
|
+
const htmlPathname = `/api/file-preview/${hash}/${fileName}`;
|
|
1553
|
+
const resourcePathname = previewKind === "html" ? null : `/api/file-preview/${hash}/resource/${fileName}`;
|
|
1554
|
+
const entryPathname = previewKind === "html" ? htmlPathname : `/api/file-preview/${hash}/${entryFileName}`;
|
|
1555
|
+
return {
|
|
1556
|
+
hash,
|
|
1557
|
+
mime,
|
|
1558
|
+
previewKind,
|
|
1559
|
+
relativePath: resolved.relativePath,
|
|
1560
|
+
resourcePathname,
|
|
1561
|
+
entryPathname,
|
|
1562
|
+
urlPath: previewKind === "html" ? htmlPathname : `${entryPathname}?file=${encodeURIComponent(fileName)}`
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
readPreviewRequest(hash, requestPath) {
|
|
1566
|
+
const session = this.sessions.get(hash);
|
|
1567
|
+
if (!session) return null;
|
|
1568
|
+
const normalized = stripLeadingSlash(requestPath);
|
|
1569
|
+
if (session.previewKind === "html") {
|
|
1570
|
+
const absolutePath$1 = resolve(session.directoryPath, normalized);
|
|
1571
|
+
if (!absolutePath$1.startsWith(session.directoryPath + "/")) return null;
|
|
1572
|
+
if (!existsSync(absolutePath$1) || !statSync(absolutePath$1).isFile()) return null;
|
|
1573
|
+
return {
|
|
1574
|
+
content: readFileSync(absolutePath$1),
|
|
1575
|
+
contentType: inferFileMime(absolutePath$1) ?? "application/octet-stream"
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
if (normalized.startsWith("resource/")) {
|
|
1579
|
+
const resourcePath = normalized.slice(9);
|
|
1580
|
+
const absolutePath$1 = resolve(session.directoryPath, resourcePath);
|
|
1581
|
+
if (!absolutePath$1.startsWith(session.directoryPath + "/")) return null;
|
|
1582
|
+
if (!existsSync(absolutePath$1) || !statSync(absolutePath$1).isFile()) return null;
|
|
1583
|
+
return {
|
|
1584
|
+
content: readFileSync(absolutePath$1),
|
|
1585
|
+
contentType: inferFileMime(absolutePath$1) ?? "application/octet-stream"
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
const assetName = normalized || session.entryFileName;
|
|
1589
|
+
if (!assetName) return null;
|
|
1590
|
+
const absolutePath = resolve(this.previewAssetsDir, assetName);
|
|
1591
|
+
if (!absolutePath.startsWith(resolve(this.previewAssetsDir) + "/")) return null;
|
|
1592
|
+
if (!existsSync(absolutePath) || !statSync(absolutePath).isFile()) return null;
|
|
1593
|
+
if (isRewritablePreviewAsset(assetName)) {
|
|
1594
|
+
const rewritten = rewritePreviewAssetPaths(readFileSync(absolutePath, "utf8"), hash);
|
|
1595
|
+
return {
|
|
1596
|
+
content: Buffer.from(rewritten, "utf8"),
|
|
1597
|
+
contentType: inferPreviewAssetContentType(absolutePath)
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
return {
|
|
1601
|
+
content: readFileSync(absolutePath),
|
|
1602
|
+
contentType: inferPreviewAssetContentType(absolutePath)
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
|
|
1453
1607
|
//#endregion
|
|
1454
1608
|
//#region src/huggingface-endpoint.ts
|
|
1455
1609
|
const DEFAULT_HUGGING_FACE_ENDPOINT = "https://huggingface.co";
|
|
@@ -5290,6 +5444,31 @@ const changeRouter = router({
|
|
|
5290
5444
|
}),
|
|
5291
5445
|
subscribeFiles: publicProcedure.input(z.object({ id: z.string() })).subscription(({ ctx, input }) => {
|
|
5292
5446
|
return createReactiveSubscriptionWithInput((id) => ctx.adapter.readChangeFiles(id))(input.id);
|
|
5447
|
+
}),
|
|
5448
|
+
writeFile: publicProcedure.input(z.object({
|
|
5449
|
+
id: z.string(),
|
|
5450
|
+
path: z.string(),
|
|
5451
|
+
content: z.string()
|
|
5452
|
+
})).mutation(async ({ ctx, input }) => {
|
|
5453
|
+
const info = resolveEntityEntryPath({
|
|
5454
|
+
projectDir: ctx.projectDir,
|
|
5455
|
+
stage: "change",
|
|
5456
|
+
changeId: input.id,
|
|
5457
|
+
path: input.path
|
|
5458
|
+
});
|
|
5459
|
+
await mkdir(dirname(info.absolutePath), { recursive: true });
|
|
5460
|
+
await writeFile(info.absolutePath, input.content, "utf-8");
|
|
5461
|
+
return { success: true };
|
|
5462
|
+
}),
|
|
5463
|
+
prepareFilePreview: publicProcedure.input(z.object({
|
|
5464
|
+
id: z.string(),
|
|
5465
|
+
path: z.string()
|
|
5466
|
+
})).query(({ ctx, input }) => {
|
|
5467
|
+
return ctx.filePreviewService.prepareEntityFilePreview({
|
|
5468
|
+
stage: "change",
|
|
5469
|
+
changeId: input.id,
|
|
5470
|
+
path: input.path
|
|
5471
|
+
});
|
|
5293
5472
|
})
|
|
5294
5473
|
});
|
|
5295
5474
|
/**
|
|
@@ -5322,9 +5501,32 @@ const archiveRouter = router({
|
|
|
5322
5501
|
return createReactiveSubscriptionWithInput(async (id) => ctx.documentService.readEntityDetail("archive", id, "view", "processed", await buildEntityReadOptions(ctx, "archive", id)))(input.id);
|
|
5323
5502
|
}),
|
|
5324
5503
|
subscribeFiles: publicProcedure.input(z.object({ id: z.string() })).subscription(({ ctx, input }) => {
|
|
5325
|
-
return createReactiveSubscriptionWithInput(
|
|
5326
|
-
|
|
5327
|
-
|
|
5504
|
+
return createReactiveSubscriptionWithInput((id) => ctx.documentService.readArchivedChangeFiles(id, "view", "source"))(input.id);
|
|
5505
|
+
}),
|
|
5506
|
+
writeFile: publicProcedure.input(z.object({
|
|
5507
|
+
id: z.string(),
|
|
5508
|
+
path: z.string(),
|
|
5509
|
+
content: z.string()
|
|
5510
|
+
})).mutation(async ({ ctx, input }) => {
|
|
5511
|
+
const info = resolveEntityEntryPath({
|
|
5512
|
+
projectDir: ctx.projectDir,
|
|
5513
|
+
stage: "archive",
|
|
5514
|
+
changeId: input.id,
|
|
5515
|
+
path: input.path
|
|
5516
|
+
});
|
|
5517
|
+
await mkdir(dirname(info.absolutePath), { recursive: true });
|
|
5518
|
+
await writeFile(info.absolutePath, input.content, "utf-8");
|
|
5519
|
+
return { success: true };
|
|
5520
|
+
}),
|
|
5521
|
+
prepareFilePreview: publicProcedure.input(z.object({
|
|
5522
|
+
id: z.string(),
|
|
5523
|
+
path: z.string()
|
|
5524
|
+
})).query(({ ctx, input }) => {
|
|
5525
|
+
return ctx.filePreviewService.prepareEntityFilePreview({
|
|
5526
|
+
stage: "archive",
|
|
5527
|
+
changeId: input.id,
|
|
5528
|
+
path: input.path
|
|
5529
|
+
});
|
|
5328
5530
|
})
|
|
5329
5531
|
});
|
|
5330
5532
|
z.object({
|
|
@@ -6616,12 +6818,12 @@ var TranslationEngineService = class {
|
|
|
6616
6818
|
}
|
|
6617
6819
|
async loadFactory(engineId, model) {
|
|
6618
6820
|
const globalSettings = await this.globalSettingsManager.readSettings();
|
|
6619
|
-
if (engineId === "local") return (await import("
|
|
6821
|
+
if (engineId === "local") return (await import("./src-DBFY1eQK.mjs")).createLocalTranslatorFactory({
|
|
6620
6822
|
defaultModel: model ?? globalSettings.translationEngines.local.model,
|
|
6621
6823
|
cacheDir: this.localCacheDir,
|
|
6622
6824
|
localOnly: true
|
|
6623
6825
|
});
|
|
6624
|
-
return (await import("
|
|
6826
|
+
return (await import("./src-DuQ_-3Kn.mjs")).createOpenAICompletionTranslatorFactory({
|
|
6625
6827
|
baseUrl: globalSettings.translationEngines.openai.baseUrl,
|
|
6626
6828
|
token: globalSettings.translationEngines.openai.token,
|
|
6627
6829
|
model: model ?? globalSettings.translationEngines.openai.model
|
|
@@ -6940,6 +7142,7 @@ function createServer(config) {
|
|
|
6940
7142
|
const kernel = config.kernel;
|
|
6941
7143
|
const hookRuntime = createHookRuntime(config.projectDir);
|
|
6942
7144
|
const documentService = new DocumentService(config.projectDir, adapter, hookRuntime);
|
|
7145
|
+
const filePreviewService = new FilePreviewService(config.projectDir, config.previewAssetsDir ?? join(__dirname, "..", "..", "web", "dist"));
|
|
6943
7146
|
const workflowInvocationService = new WorkflowInvocationService({
|
|
6944
7147
|
projectDir: config.projectDir,
|
|
6945
7148
|
hookRuntime,
|
|
@@ -7054,6 +7257,21 @@ function createServer(config) {
|
|
|
7054
7257
|
"Cache-Control": "private, max-age=31536000, immutable"
|
|
7055
7258
|
} });
|
|
7056
7259
|
});
|
|
7260
|
+
app.get("/api/file-preview/:hash/*", async (c) => {
|
|
7261
|
+
const hash = c.req.param("hash");
|
|
7262
|
+
const prefix = `/api/file-preview/${hash}/`;
|
|
7263
|
+
const requestPath = c.req.path.startsWith(prefix) ? c.req.path.slice(prefix.length) : "";
|
|
7264
|
+
const asset = filePreviewService.readPreviewRequest(hash, requestPath);
|
|
7265
|
+
if (!asset) return c.notFound();
|
|
7266
|
+
const stream = new ReadableStream({ start(controller) {
|
|
7267
|
+
controller.enqueue(new Uint8Array(asset.content));
|
|
7268
|
+
controller.close();
|
|
7269
|
+
} });
|
|
7270
|
+
return new Response(stream, {
|
|
7271
|
+
status: 200,
|
|
7272
|
+
headers: { "Content-Type": asset.contentType }
|
|
7273
|
+
});
|
|
7274
|
+
});
|
|
7057
7275
|
app.use("/trpc/*", async (c) => {
|
|
7058
7276
|
return await fetchRequestHandler({
|
|
7059
7277
|
endpoint: "/trpc",
|
|
@@ -7073,6 +7291,7 @@ function createServer(config) {
|
|
|
7073
7291
|
customSoundService,
|
|
7074
7292
|
globalSettingsManager,
|
|
7075
7293
|
translationCacheService,
|
|
7294
|
+
filePreviewService,
|
|
7076
7295
|
translationEngineService,
|
|
7077
7296
|
localModelAssetService,
|
|
7078
7297
|
gitWorktreeHandoff: config.gitWorktreeHandoff,
|
|
@@ -7095,6 +7314,7 @@ function createServer(config) {
|
|
|
7095
7314
|
customSoundService,
|
|
7096
7315
|
globalSettingsManager,
|
|
7097
7316
|
translationCacheService,
|
|
7317
|
+
filePreviewService,
|
|
7098
7318
|
translationEngineService,
|
|
7099
7319
|
localModelAssetService,
|
|
7100
7320
|
gitWorktreeHandoff: config.gitWorktreeHandoff,
|
|
@@ -7116,7 +7336,9 @@ function createServer(config) {
|
|
|
7116
7336
|
customSoundService,
|
|
7117
7337
|
globalSettingsManager,
|
|
7118
7338
|
translationCacheService,
|
|
7339
|
+
filePreviewService,
|
|
7119
7340
|
translationEngineService,
|
|
7341
|
+
localModelAssetService,
|
|
7120
7342
|
hookRuntime,
|
|
7121
7343
|
watcher,
|
|
7122
7344
|
createContext,
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import "@openspecui/core/local-download-profiles";
|
|
4
|
+
|
|
5
|
+
//#region ../local-translator/src/index.ts
|
|
6
|
+
const DEFAULT_MODEL = "Xenova/nllb-200-distilled-600M";
|
|
7
|
+
var LocalTranslatorFactory = class {
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.options = options;
|
|
10
|
+
}
|
|
11
|
+
async prepare(options) {
|
|
12
|
+
await (await loadTranslationPipeline(options.model || this.options.defaultModel || DEFAULT_MODEL, options.monitor, this.options.cacheDir, options.dtype ?? this.options.dtype, this.options.localOnly, options.runtimeConfig)).dispose?.();
|
|
13
|
+
}
|
|
14
|
+
async create(options) {
|
|
15
|
+
return new LocalTranslator(await loadTranslationPipeline(options.model || this.options.defaultModel || DEFAULT_MODEL, options.monitor, this.options.cacheDir, options.dtype ?? this.options.dtype, this.options.localOnly, options.runtimeConfig), {
|
|
16
|
+
sourceLanguage: options.sourceLanguage,
|
|
17
|
+
targetLanguage: options.targetLanguage
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
function createLocalTranslatorFactory(options = {}) {
|
|
22
|
+
return new LocalTranslatorFactory(options);
|
|
23
|
+
}
|
|
24
|
+
var LocalTranslator = class {
|
|
25
|
+
constructor(pipeline, languages) {
|
|
26
|
+
this.pipeline = pipeline;
|
|
27
|
+
this.languages = languages;
|
|
28
|
+
}
|
|
29
|
+
async *batchTranslate(inputs, options) {
|
|
30
|
+
for (const [index, input] of inputs.entries()) {
|
|
31
|
+
throwIfAborted(options?.signal);
|
|
32
|
+
const result = await this.pipeline(input, {
|
|
33
|
+
src_lang: this.languages.sourceLanguage,
|
|
34
|
+
tgt_lang: this.languages.targetLanguage
|
|
35
|
+
});
|
|
36
|
+
throwIfAborted(options?.signal);
|
|
37
|
+
yield {
|
|
38
|
+
index,
|
|
39
|
+
output: readTranslatedText(result)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
destroy() {
|
|
44
|
+
this.pipeline.dispose?.();
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
async function loadTranslationPipeline(model, monitor, cacheDir, dtype, localOnly = false, runtimeConfig) {
|
|
48
|
+
monitor?.setStatus({ message: `Loading local model ${model}.` });
|
|
49
|
+
const transformers = await import("@huggingface/transformers");
|
|
50
|
+
if (cacheDir && transformers.env) {
|
|
51
|
+
transformers.env.cacheDir = cacheDir;
|
|
52
|
+
transformers.env.localModelPath = join(cacheDir, "models");
|
|
53
|
+
}
|
|
54
|
+
if (transformers.env) {
|
|
55
|
+
transformers.env.allowLocalModels = true;
|
|
56
|
+
transformers.env.allowRemoteModels = !localOnly;
|
|
57
|
+
}
|
|
58
|
+
const runtimeModel = localOnly && cacheDir ? join(cacheDir, "models", model) : model;
|
|
59
|
+
const progressCallback = monitor ? (event) => {
|
|
60
|
+
const progress = readProgress(event);
|
|
61
|
+
monitor.setStatus({
|
|
62
|
+
message: progress === void 0 ? `Downloading local model ${model}.` : `Downloading local model ${model} ${Math.round(progress * 100)}%.`,
|
|
63
|
+
...progress === void 0 ? {} : { progress }
|
|
64
|
+
});
|
|
65
|
+
} : void 0;
|
|
66
|
+
const pipeline = await transformers.pipeline("translation", runtimeModel, {
|
|
67
|
+
config: runtimeConfig ?? await readLocalRuntimeConfig({
|
|
68
|
+
cacheDir,
|
|
69
|
+
model,
|
|
70
|
+
localOnly
|
|
71
|
+
}),
|
|
72
|
+
...dtype ? { dtype } : {},
|
|
73
|
+
...localOnly ? { local_files_only: true } : {},
|
|
74
|
+
...progressCallback ? { progress_callback: progressCallback } : {}
|
|
75
|
+
});
|
|
76
|
+
monitor?.setStatus({
|
|
77
|
+
message: `Local model ${model} is ready.`,
|
|
78
|
+
progress: 1
|
|
79
|
+
});
|
|
80
|
+
return pipeline;
|
|
81
|
+
}
|
|
82
|
+
async function readLocalRuntimeConfig(input) {
|
|
83
|
+
if (!input.localOnly || !input.cacheDir) return void 0;
|
|
84
|
+
const configPath = join(input.cacheDir, "models", input.model, "config.json");
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(await readFile(configPath, "utf8"));
|
|
87
|
+
} catch {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function readTranslatedText(value) {
|
|
92
|
+
if (typeof value === "string") return value;
|
|
93
|
+
if (Array.isArray(value)) {
|
|
94
|
+
const first = value[0];
|
|
95
|
+
if (first && typeof first === "object") {
|
|
96
|
+
const text = first.translation_text;
|
|
97
|
+
if (typeof text === "string") return text;
|
|
98
|
+
const generated = first.generated_text;
|
|
99
|
+
if (typeof generated === "string") return generated;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (value && typeof value === "object") {
|
|
103
|
+
const text = value.translation_text;
|
|
104
|
+
if (typeof text === "string") return text;
|
|
105
|
+
const generated = value.generated_text;
|
|
106
|
+
if (typeof generated === "string") return generated;
|
|
107
|
+
}
|
|
108
|
+
return String(value);
|
|
109
|
+
}
|
|
110
|
+
function readProgress(event) {
|
|
111
|
+
if (!event || typeof event !== "object") return void 0;
|
|
112
|
+
const record = event;
|
|
113
|
+
if (typeof record.progress === "number") return Math.max(0, Math.min(1, record.progress / (record.progress > 1 ? 100 : 1)));
|
|
114
|
+
if (typeof record.loaded === "number" && typeof record.total === "number" && record.total > 0) return Math.max(0, Math.min(1, record.loaded / record.total));
|
|
115
|
+
}
|
|
116
|
+
function throwIfAborted(signal) {
|
|
117
|
+
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
//#endregion
|
|
121
|
+
export { createLocalTranslatorFactory };
|