@openspecui/server 3.7.1 → 3.9.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 +2172 -33
- package/package.json +7 -1
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { CliExecutor, CodeEditorThemeSchema, ConfigManager, CustomSoundHashSchema, DASHBOARD_METRIC_KEYS, DashboardConfigSchema, DocumentTranslationConfigSchema, GitConfigSchema, GlobalSettingsManager, MarkdownParser, NotificationPublishInputSchema, NotificationSettingsSchema, OPENSPECUI_HOOKS_VERSION, OpenSpecAdapter, OpenSpecUIGlobalSettingsSchema, OpenSpecWatcher, OpsxConfigSchema, OpsxKernel, PtyClientMessageSchema, ReactiveContext, TerminalConfigSchema, TerminalControlParser, TerminalRendererEngineSchema, TranslationCacheReadInputSchema, TranslationCacheSettingsSchema, TranslationCacheWriteInputSchema, buildBackendHealthPayload, getAllTools, getAvailableTools, getConfiguredTools, getDefaultCliCommandString, getDetectedProjectTools, getToolInitStates, getWatcherRuntimeStatus, initWatcherPool, isWatcherPoolInitialized, parseOpsxEntityMetadata, parseOpsxSchemaDetail, resolveTerminalShellDefaults, sniffGlobalCli, subscribeWatcherRuntimeStatus, terminalNotificationEventToPublishInput } from "@openspecui/core";
|
|
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
2
|
import { basename, dirname, join, matchesGlob, relative, resolve, sep } from "node:path";
|
|
3
|
-
import { access, mkdir, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
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";
|
|
6
6
|
import { serve } from "@hono/node-server";
|
|
@@ -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 { readFileSync } from "node:fs";
|
|
11
|
+
import { existsSync, readFileSync } 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";
|
|
@@ -16,13 +16,15 @@ import { homedir, platform } from "node:os";
|
|
|
16
16
|
import { EventEmitter } from "node:events";
|
|
17
17
|
import { execFile } from "node:child_process";
|
|
18
18
|
import { promisify } from "node:util";
|
|
19
|
+
import { downloadFile, fileDownloadInfo, listFiles } from "@huggingface/hub";
|
|
20
|
+
import { observable } from "@trpc/server/observable";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
import { Agent, EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
|
|
19
23
|
import { NotificationGroupKeySchema, NotificationPublishInputSchema as NotificationPublishInputSchema$1, getNotificationGroupKey } from "@openspecui/core/notifications";
|
|
20
24
|
import * as pty from "@lydell/node-pty";
|
|
21
25
|
import { EventEmitter as EventEmitter$1 } from "events";
|
|
22
26
|
import { SearchQuerySchema } from "@openspecui/search";
|
|
23
27
|
import { initTRPC } from "@trpc/server";
|
|
24
|
-
import { observable } from "@trpc/server/observable";
|
|
25
|
-
import { z } from "zod";
|
|
26
28
|
import { NodeWorkerSearchProvider } from "@openspecui/search/node";
|
|
27
29
|
|
|
28
30
|
//#region src/document-service.ts
|
|
@@ -355,7 +357,7 @@ function normalizeHooksModule(moduleValue) {
|
|
|
355
357
|
onRunWorkflow: isOnRunWorkflowHook(record.onRunWorkflow) ? record.onRunWorkflow : isOnRunWorkflowHook(defaultRecord.onRunWorkflow) ? defaultRecord.onRunWorkflow : isOnRunWorkflowHook(moduleExportsRecord.onRunWorkflow) ? moduleExportsRecord.onRunWorkflow : void 0
|
|
356
358
|
};
|
|
357
359
|
}
|
|
358
|
-
async function pathExists$
|
|
360
|
+
async function pathExists$2(path) {
|
|
359
361
|
try {
|
|
360
362
|
await access(path);
|
|
361
363
|
return true;
|
|
@@ -384,7 +386,7 @@ var ProjectHookRuntime = class {
|
|
|
384
386
|
await Promise.allSettled(callbacks.map((cleanup) => cleanup()));
|
|
385
387
|
}
|
|
386
388
|
async loadFresh() {
|
|
387
|
-
if (!await pathExists$
|
|
389
|
+
if (!await pathExists$2(this.hooksPath)) return {};
|
|
388
390
|
const { tsImport } = await import("tsx/esm/api");
|
|
389
391
|
return normalizeHooksModule(await tsImport(`${pathToFileURL(this.hooksPath).href}?t=${Date.now()}`, { parentURL: pathToFileURL(this.hooksPath).href }));
|
|
390
392
|
}
|
|
@@ -1448,6 +1450,1783 @@ async function buildEntityReadOptions(ctx, stage, id) {
|
|
|
1448
1450
|
}
|
|
1449
1451
|
}
|
|
1450
1452
|
|
|
1453
|
+
//#endregion
|
|
1454
|
+
//#region src/huggingface-endpoint.ts
|
|
1455
|
+
const DEFAULT_HUGGING_FACE_ENDPOINT = "https://huggingface.co";
|
|
1456
|
+
function normalizeHuggingFaceEndpoint(endpoint) {
|
|
1457
|
+
const trimmed = endpoint?.trim();
|
|
1458
|
+
if (!trimmed) return DEFAULT_HUGGING_FACE_ENDPOINT;
|
|
1459
|
+
return trimmed.replace(/\/+$/, "");
|
|
1460
|
+
}
|
|
1461
|
+
function buildHuggingFaceApiBaseUrl(endpoint) {
|
|
1462
|
+
return `${normalizeHuggingFaceEndpoint(endpoint)}/api`;
|
|
1463
|
+
}
|
|
1464
|
+
function buildTransformersRemoteHost(endpoint) {
|
|
1465
|
+
return `${normalizeHuggingFaceEndpoint(endpoint)}/`;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
//#endregion
|
|
1469
|
+
//#region src/local-model-asset-store.ts
|
|
1470
|
+
const LocalModelAssetIndexSchema = LocalModelAssetStateSchema.array();
|
|
1471
|
+
var LocalModelAssetStore = class {
|
|
1472
|
+
constructor(options) {
|
|
1473
|
+
this.options = options;
|
|
1474
|
+
}
|
|
1475
|
+
getIndexPath() {
|
|
1476
|
+
return this.options.indexPath;
|
|
1477
|
+
}
|
|
1478
|
+
async readAll() {
|
|
1479
|
+
try {
|
|
1480
|
+
const content = await readFile(this.options.indexPath, "utf8");
|
|
1481
|
+
const parsed = JSON.parse(content);
|
|
1482
|
+
const result = LocalModelAssetIndexSchema.safeParse(parsed);
|
|
1483
|
+
return result.success ? result.data : [];
|
|
1484
|
+
} catch {
|
|
1485
|
+
return [];
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
async readMap() {
|
|
1489
|
+
return new Map((await this.readAll()).map((state) => [state.modelId, state]));
|
|
1490
|
+
}
|
|
1491
|
+
async writeAll(states) {
|
|
1492
|
+
const normalized = LocalModelAssetIndexSchema.parse([...states].sort((left, right) => left.modelId.localeCompare(right.modelId)));
|
|
1493
|
+
const serialized = JSON.stringify(normalized, null, 2);
|
|
1494
|
+
await mkdir(dirname(this.options.indexPath), { recursive: true });
|
|
1495
|
+
const tempPath = `${this.options.indexPath}.${process.pid}.${Date.now()}.tmp`;
|
|
1496
|
+
await writeFile(tempPath, `${serialized}\n`, "utf8");
|
|
1497
|
+
await rename(tempPath, this.options.indexPath);
|
|
1498
|
+
clearCache();
|
|
1499
|
+
}
|
|
1500
|
+
async upsert(state) {
|
|
1501
|
+
const states = await this.readMap();
|
|
1502
|
+
states.set(state.modelId, LocalModelAssetStateSchema.parse(state));
|
|
1503
|
+
await this.writeAll([...states.values()]);
|
|
1504
|
+
}
|
|
1505
|
+
async remove(modelId) {
|
|
1506
|
+
const states = await this.readMap();
|
|
1507
|
+
if (!states.delete(modelId)) return;
|
|
1508
|
+
await this.writeAll([...states.values()]);
|
|
1509
|
+
}
|
|
1510
|
+
};
|
|
1511
|
+
|
|
1512
|
+
//#endregion
|
|
1513
|
+
//#region src/translation-cache-path.ts
|
|
1514
|
+
function getDefaultTranslationCacheDatabasePath() {
|
|
1515
|
+
return join(getOpenSpecUICacheDir(), "translation-cache.sqlite");
|
|
1516
|
+
}
|
|
1517
|
+
function getOpenSpecUICacheDir() {
|
|
1518
|
+
const currentPlatform = platform();
|
|
1519
|
+
if (currentPlatform === "darwin") return join(homedir(), "Library", "Caches", "openspecui");
|
|
1520
|
+
if (currentPlatform === "win32") return join(process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local"), "OpenSpecUI", "Cache");
|
|
1521
|
+
return join(process.env.XDG_CACHE_HOME || join(homedir(), ".cache"), "openspecui");
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
//#endregion
|
|
1525
|
+
//#region src/local-model-cache-path.ts
|
|
1526
|
+
function getDefaultLocalModelCacheRoot() {
|
|
1527
|
+
return join(getOpenSpecUICacheDir(), "translation-engines", "local");
|
|
1528
|
+
}
|
|
1529
|
+
function getDefaultLocalModelCacheDir() {
|
|
1530
|
+
return join(getDefaultLocalModelCacheRoot(), "hf-cache");
|
|
1531
|
+
}
|
|
1532
|
+
function getDefaultLocalModelIndexPath() {
|
|
1533
|
+
return join(getDefaultLocalModelCacheRoot(), "models.json");
|
|
1534
|
+
}
|
|
1535
|
+
function getDefaultLocalModelFetchCachePath() {
|
|
1536
|
+
return join(getDefaultLocalModelCacheRoot(), "fetch-cache.json");
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
//#endregion
|
|
1540
|
+
//#region src/local-model-fetch-cache-store.ts
|
|
1541
|
+
const RawJsonRecordSchema = z.record(z.string(), z.unknown());
|
|
1542
|
+
const HttpHeaderRecordSchema = z.record(z.string(), z.string());
|
|
1543
|
+
const LocalModelFetchCacheQueryContextSchema = z.object({
|
|
1544
|
+
query: z.string().optional(),
|
|
1545
|
+
sourceLanguage: z.string().optional(),
|
|
1546
|
+
targetLanguage: z.string().optional()
|
|
1547
|
+
});
|
|
1548
|
+
const LocalModelProviderFetchRecordSchema = z.object({
|
|
1549
|
+
id: z.string().min(1),
|
|
1550
|
+
source: z.literal("huggingface"),
|
|
1551
|
+
fetchedAt: z.number().int().nonnegative(),
|
|
1552
|
+
request: z.object({
|
|
1553
|
+
method: z.literal("GET"),
|
|
1554
|
+
url: z.string().min(1),
|
|
1555
|
+
queryContext: LocalModelFetchCacheQueryContextSchema.optional()
|
|
1556
|
+
}),
|
|
1557
|
+
response: z.object({
|
|
1558
|
+
status: z.number().int().nonnegative(),
|
|
1559
|
+
ok: z.boolean(),
|
|
1560
|
+
headers: HttpHeaderRecordSchema,
|
|
1561
|
+
bodyText: z.string()
|
|
1562
|
+
})
|
|
1563
|
+
});
|
|
1564
|
+
const LocalModelFetchCacheRecordSchema = z.object({
|
|
1565
|
+
modelId: z.string().min(1),
|
|
1566
|
+
source: z.literal("huggingface"),
|
|
1567
|
+
listItemRaw: RawJsonRecordSchema.optional(),
|
|
1568
|
+
detailRaw: RawJsonRecordSchema.optional(),
|
|
1569
|
+
queryContext: LocalModelFetchCacheQueryContextSchema.optional(),
|
|
1570
|
+
listFetchedAt: z.number().int().nonnegative().optional(),
|
|
1571
|
+
detailFetchedAt: z.number().int().nonnegative().optional(),
|
|
1572
|
+
updatedAt: z.number().int().nonnegative()
|
|
1573
|
+
});
|
|
1574
|
+
const LocalModelFetchCacheFileSchema = z.object({
|
|
1575
|
+
version: z.literal(1).default(1),
|
|
1576
|
+
fetches: z.array(LocalModelProviderFetchRecordSchema).default([]),
|
|
1577
|
+
records: z.array(LocalModelFetchCacheRecordSchema).default([])
|
|
1578
|
+
});
|
|
1579
|
+
var LocalModelFetchCacheStore = class {
|
|
1580
|
+
now;
|
|
1581
|
+
constructor(options) {
|
|
1582
|
+
this.options = options;
|
|
1583
|
+
this.now = options.now ?? Date.now;
|
|
1584
|
+
}
|
|
1585
|
+
async readAll() {
|
|
1586
|
+
return (await this.readFile()).records;
|
|
1587
|
+
}
|
|
1588
|
+
async readFetches() {
|
|
1589
|
+
return (await this.readFile()).fetches;
|
|
1590
|
+
}
|
|
1591
|
+
async readFile() {
|
|
1592
|
+
try {
|
|
1593
|
+
const content = await readFile(this.options.cachePath, "utf8");
|
|
1594
|
+
const parsed = JSON.parse(content);
|
|
1595
|
+
const result = LocalModelFetchCacheFileSchema.safeParse(parsed);
|
|
1596
|
+
return result.success ? result.data : LocalModelFetchCacheFileSchema.parse({});
|
|
1597
|
+
} catch {
|
|
1598
|
+
return LocalModelFetchCacheFileSchema.parse({});
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
async read(modelId) {
|
|
1602
|
+
return (await this.readMap()).get(modelId) ?? null;
|
|
1603
|
+
}
|
|
1604
|
+
async readMap() {
|
|
1605
|
+
return new Map((await this.readAll()).map((record) => [record.modelId, record]));
|
|
1606
|
+
}
|
|
1607
|
+
async writeAll(records) {
|
|
1608
|
+
const current = await this.readFile();
|
|
1609
|
+
await this.writeFile({
|
|
1610
|
+
fetches: current.fetches,
|
|
1611
|
+
records
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
async writeFile(input) {
|
|
1615
|
+
const normalized = LocalModelFetchCacheFileSchema.parse({
|
|
1616
|
+
version: 1,
|
|
1617
|
+
fetches: [...input.fetches].sort((left, right) => left.id.localeCompare(right.id)),
|
|
1618
|
+
records: [...input.records].sort((left, right) => left.modelId.localeCompare(right.modelId))
|
|
1619
|
+
});
|
|
1620
|
+
await mkdir(dirname(this.options.cachePath), { recursive: true });
|
|
1621
|
+
await writeFile(this.options.cachePath, `${JSON.stringify(normalized, null, 2)}\n`, "utf8");
|
|
1622
|
+
clearCache();
|
|
1623
|
+
}
|
|
1624
|
+
async upsertProviderFetch(input) {
|
|
1625
|
+
const current = await this.readFile();
|
|
1626
|
+
const id = buildProviderFetchId(input.url);
|
|
1627
|
+
const fetches = new Map(current.fetches.map((record) => [record.id, record]));
|
|
1628
|
+
fetches.set(id, LocalModelProviderFetchRecordSchema.parse({
|
|
1629
|
+
id,
|
|
1630
|
+
source: "huggingface",
|
|
1631
|
+
fetchedAt: this.now(),
|
|
1632
|
+
request: {
|
|
1633
|
+
method: "GET",
|
|
1634
|
+
url: input.url,
|
|
1635
|
+
queryContext: input.queryContext
|
|
1636
|
+
},
|
|
1637
|
+
response: {
|
|
1638
|
+
status: input.status,
|
|
1639
|
+
ok: input.ok,
|
|
1640
|
+
headers: input.headers,
|
|
1641
|
+
bodyText: input.bodyText
|
|
1642
|
+
}
|
|
1643
|
+
}));
|
|
1644
|
+
await this.writeFile({
|
|
1645
|
+
fetches: [...fetches.values()],
|
|
1646
|
+
records: current.records
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
async upsertListItem(input) {
|
|
1650
|
+
const records = await this.readMap();
|
|
1651
|
+
const current = records.get(input.modelId);
|
|
1652
|
+
records.set(input.modelId, LocalModelFetchCacheRecordSchema.parse({
|
|
1653
|
+
...current,
|
|
1654
|
+
modelId: input.modelId,
|
|
1655
|
+
source: "huggingface",
|
|
1656
|
+
listItemRaw: input.raw,
|
|
1657
|
+
queryContext: input.queryContext ?? current?.queryContext,
|
|
1658
|
+
listFetchedAt: this.now(),
|
|
1659
|
+
updatedAt: this.now()
|
|
1660
|
+
}));
|
|
1661
|
+
await this.writeAll([...records.values()]);
|
|
1662
|
+
}
|
|
1663
|
+
async upsertDetail(input) {
|
|
1664
|
+
const records = await this.readMap();
|
|
1665
|
+
const current = records.get(input.modelId);
|
|
1666
|
+
records.set(input.modelId, LocalModelFetchCacheRecordSchema.parse({
|
|
1667
|
+
...current,
|
|
1668
|
+
modelId: input.modelId,
|
|
1669
|
+
source: "huggingface",
|
|
1670
|
+
detailRaw: input.raw,
|
|
1671
|
+
queryContext: input.queryContext ?? current?.queryContext,
|
|
1672
|
+
detailFetchedAt: this.now(),
|
|
1673
|
+
updatedAt: this.now()
|
|
1674
|
+
}));
|
|
1675
|
+
await this.writeAll([...records.values()]);
|
|
1676
|
+
}
|
|
1677
|
+
};
|
|
1678
|
+
function buildProviderFetchId(url) {
|
|
1679
|
+
return `huggingface:GET:${url}`;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
//#endregion
|
|
1683
|
+
//#region src/local-model-local-cache.ts
|
|
1684
|
+
function getTransformersLocalModelPath(cacheDir, modelId) {
|
|
1685
|
+
return join(cacheDir, "models", modelId);
|
|
1686
|
+
}
|
|
1687
|
+
function getTransformersFileCacheModelPath(cacheDir, modelId) {
|
|
1688
|
+
return join(cacheDir, modelId);
|
|
1689
|
+
}
|
|
1690
|
+
async function readLocalModelFileStatus(input) {
|
|
1691
|
+
const files = await Promise.all(input.files.map(async (file) => ({
|
|
1692
|
+
file,
|
|
1693
|
+
cached: await pathExists$1(join(getTransformersLocalModelPath(input.cacheDir, input.modelId), file)) || await pathExists$1(join(getTransformersFileCacheModelPath(input.cacheDir, input.modelId), file))
|
|
1694
|
+
})));
|
|
1695
|
+
return {
|
|
1696
|
+
allCached: files.length > 0 && files.every((file) => file.cached),
|
|
1697
|
+
files
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
async function pathExists$1(path) {
|
|
1701
|
+
return stat(path).then(() => true, () => false);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
//#endregion
|
|
1705
|
+
//#region src/local-model-runtime.ts
|
|
1706
|
+
async function configureTransformersRuntime(transformers, cacheDir) {
|
|
1707
|
+
await mkdir(cacheDir, { recursive: true });
|
|
1708
|
+
transformers.env.cacheDir = cacheDir;
|
|
1709
|
+
transformers.env.allowLocalModels = false;
|
|
1710
|
+
transformers.env.localModelPath = join(cacheDir, "models");
|
|
1711
|
+
}
|
|
1712
|
+
async function resolveLocalModelRuntimePlan(input) {
|
|
1713
|
+
await configureTransformersRuntime(input.transformers, input.cacheDir);
|
|
1714
|
+
const hubUrl = normalizeHuggingFaceEndpoint(input.hfEndpoint);
|
|
1715
|
+
const repositoryFiles = await readHuggingFaceRepositoryFiles({
|
|
1716
|
+
selectedGroupId: input.selectedGroupId,
|
|
1717
|
+
modelId: input.modelId,
|
|
1718
|
+
hubUrl,
|
|
1719
|
+
fetchCacheStore: input.fetchCacheStore
|
|
1720
|
+
});
|
|
1721
|
+
return buildLocalDownloadPlanFromRepositoryFiles({
|
|
1722
|
+
modelId: input.modelId,
|
|
1723
|
+
selectedGroupId: input.selectedGroupId,
|
|
1724
|
+
files: repositoryFiles
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
async function resolveLocalModelRuntimePlanFromProject(input) {
|
|
1728
|
+
const transformers = await (input.loadTransformersModule ?? loadLocalTransformersModule)(input.projectDir, input.globalSettingsManager);
|
|
1729
|
+
const settings = await input.globalSettingsManager.readSettings();
|
|
1730
|
+
return resolveLocalModelRuntimePlan({
|
|
1731
|
+
modelId: input.modelId,
|
|
1732
|
+
transformers,
|
|
1733
|
+
cacheDir: input.cacheDir ?? getDefaultLocalModelCacheDir(),
|
|
1734
|
+
selectedGroupId: input.selectedGroupId,
|
|
1735
|
+
hfEndpoint: settings.translationEngines.local?.hfEndpoint,
|
|
1736
|
+
fetchCacheStore: input.fetchCacheStore
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
async function readHuggingFaceRepositoryFiles(input) {
|
|
1740
|
+
let lastError;
|
|
1741
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1742
|
+
try {
|
|
1743
|
+
const files = [];
|
|
1744
|
+
for await (const entry of listFiles({
|
|
1745
|
+
repo: {
|
|
1746
|
+
type: "model",
|
|
1747
|
+
name: input.modelId
|
|
1748
|
+
},
|
|
1749
|
+
recursive: true,
|
|
1750
|
+
expand: true,
|
|
1751
|
+
hubUrl: input.hubUrl,
|
|
1752
|
+
fetch: input.fetchCacheStore ? createProviderFetchCache(input.fetchCacheStore) : void 0
|
|
1753
|
+
})) {
|
|
1754
|
+
if (entry.type !== "file") continue;
|
|
1755
|
+
files.push({
|
|
1756
|
+
path: entry.path,
|
|
1757
|
+
sizeBytes: entry.lfs?.size ?? entry.size
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
if (files.length > 0) return files;
|
|
1761
|
+
lastError = /* @__PURE__ */ new Error(`No repository files were returned for ${input.modelId}.`);
|
|
1762
|
+
} catch (error) {
|
|
1763
|
+
lastError = error;
|
|
1764
|
+
}
|
|
1765
|
+
if (attempt < 2) await delay$2(300 * (attempt + 1));
|
|
1766
|
+
}
|
|
1767
|
+
const cachedFiles = await readCachedHuggingFaceRepositoryFiles(input);
|
|
1768
|
+
if (cachedFiles.length > 0) return cachedFiles;
|
|
1769
|
+
throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error(`Unable to read repository files for ${input.modelId}.`);
|
|
1770
|
+
}
|
|
1771
|
+
async function readCachedHuggingFaceRepositoryFiles(input) {
|
|
1772
|
+
if (!input.fetchCacheStore) return [];
|
|
1773
|
+
const record = await input.fetchCacheStore.read(input.modelId);
|
|
1774
|
+
if (!record?.detailRaw) return [];
|
|
1775
|
+
return extractRepositoryFilesFromCachedDetail(record.detailRaw);
|
|
1776
|
+
}
|
|
1777
|
+
function extractRepositoryFilesFromCachedDetail(raw) {
|
|
1778
|
+
const siblings = Array.isArray(raw.siblings) ? raw.siblings : [];
|
|
1779
|
+
const files = [];
|
|
1780
|
+
for (const sibling of siblings) {
|
|
1781
|
+
if (!sibling || typeof sibling !== "object") continue;
|
|
1782
|
+
const record = sibling;
|
|
1783
|
+
const path = typeof record.rfilename === "string" ? record.rfilename : null;
|
|
1784
|
+
if (!path) continue;
|
|
1785
|
+
files.push({
|
|
1786
|
+
path,
|
|
1787
|
+
sizeBytes: typeof record.size === "number" && Number.isFinite(record.size) ? record.size : void 0
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
return files;
|
|
1791
|
+
}
|
|
1792
|
+
function createProviderFetchCache(fetchCacheStore) {
|
|
1793
|
+
return async (input, init) => {
|
|
1794
|
+
const response = await fetch(input, init);
|
|
1795
|
+
const url = normalizeRequestUrl(input);
|
|
1796
|
+
if (!url.includes("/api/models/") || !url.includes("/tree/")) return response;
|
|
1797
|
+
await fetchCacheStore.upsertProviderFetch({
|
|
1798
|
+
url,
|
|
1799
|
+
status: response.status,
|
|
1800
|
+
ok: response.ok,
|
|
1801
|
+
headers: headersToRecord$1(response.headers),
|
|
1802
|
+
bodyText: await response.clone().text()
|
|
1803
|
+
});
|
|
1804
|
+
return response;
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
function normalizeRequestUrl(input) {
|
|
1808
|
+
if (typeof input === "string") return input;
|
|
1809
|
+
if (input instanceof URL) return input.href;
|
|
1810
|
+
return input.url;
|
|
1811
|
+
}
|
|
1812
|
+
function headersToRecord$1(headers) {
|
|
1813
|
+
return Object.fromEntries(headers.entries());
|
|
1814
|
+
}
|
|
1815
|
+
function delay$2(ms) {
|
|
1816
|
+
return new Promise((resolve$1) => setTimeout(resolve$1, ms));
|
|
1817
|
+
}
|
|
1818
|
+
async function loadLocalTransformersModule(_projectDir, _globalSettingsManager) {
|
|
1819
|
+
return await import("@huggingface/transformers");
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
//#endregion
|
|
1823
|
+
//#region src/network-dispatcher.ts
|
|
1824
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
|
|
1825
|
+
let sharedDispatcher = null;
|
|
1826
|
+
function createProxyAwareDispatcher() {
|
|
1827
|
+
if (hasProxyEnvironment()) return new EnvHttpProxyAgent({
|
|
1828
|
+
httpProxy: process.env.http_proxy ?? process.env.HTTP_PROXY,
|
|
1829
|
+
httpsProxy: process.env.https_proxy ?? process.env.HTTPS_PROXY,
|
|
1830
|
+
noProxy: process.env.no_proxy ?? process.env.NO_PROXY,
|
|
1831
|
+
connectTimeout: DEFAULT_CONNECT_TIMEOUT_MS
|
|
1832
|
+
});
|
|
1833
|
+
return new Agent({ connectTimeout: DEFAULT_CONNECT_TIMEOUT_MS });
|
|
1834
|
+
}
|
|
1835
|
+
function ensureProxyAwareFetchDispatcher() {
|
|
1836
|
+
if (sharedDispatcher) return sharedDispatcher;
|
|
1837
|
+
sharedDispatcher = createProxyAwareDispatcher();
|
|
1838
|
+
setGlobalDispatcher(sharedDispatcher);
|
|
1839
|
+
return sharedDispatcher;
|
|
1840
|
+
}
|
|
1841
|
+
function hasProxyEnvironment() {
|
|
1842
|
+
return Boolean(process.env.http_proxy || process.env.HTTP_PROXY || process.env.https_proxy || process.env.HTTPS_PROXY || process.env.all_proxy || process.env.ALL_PROXY);
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
//#endregion
|
|
1846
|
+
//#region src/network-retry.ts
|
|
1847
|
+
const RETRYABLE_STATUS_CODES = new Set([
|
|
1848
|
+
408,
|
|
1849
|
+
409,
|
|
1850
|
+
425,
|
|
1851
|
+
429,
|
|
1852
|
+
500,
|
|
1853
|
+
502,
|
|
1854
|
+
503,
|
|
1855
|
+
504,
|
|
1856
|
+
520,
|
|
1857
|
+
521,
|
|
1858
|
+
522,
|
|
1859
|
+
523,
|
|
1860
|
+
524
|
|
1861
|
+
]);
|
|
1862
|
+
const RETRYABLE_ERROR_CODES = new Set([
|
|
1863
|
+
"ECONNABORTED",
|
|
1864
|
+
"ECONNREFUSED",
|
|
1865
|
+
"ECONNRESET",
|
|
1866
|
+
"EAI_AGAIN",
|
|
1867
|
+
"ENETDOWN",
|
|
1868
|
+
"ENETRESET",
|
|
1869
|
+
"ENETUNREACH",
|
|
1870
|
+
"ENOTFOUND",
|
|
1871
|
+
"ETIMEDOUT",
|
|
1872
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
1873
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
1874
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
1875
|
+
"UND_ERR_SOCKET"
|
|
1876
|
+
]);
|
|
1877
|
+
const RETRYABLE_MESSAGE_FRAGMENTS = [
|
|
1878
|
+
"fetch failed",
|
|
1879
|
+
"timeout",
|
|
1880
|
+
"timed out",
|
|
1881
|
+
"socket hang up",
|
|
1882
|
+
"econnreset",
|
|
1883
|
+
"econnrefused",
|
|
1884
|
+
"eai_again",
|
|
1885
|
+
"enotfound",
|
|
1886
|
+
"terminated",
|
|
1887
|
+
"too many requests",
|
|
1888
|
+
"rate limit",
|
|
1889
|
+
"bad gateway",
|
|
1890
|
+
"service unavailable",
|
|
1891
|
+
"gateway timeout",
|
|
1892
|
+
"temporarily unavailable",
|
|
1893
|
+
"connection reset",
|
|
1894
|
+
"connection refused"
|
|
1895
|
+
];
|
|
1896
|
+
function isRetryableNetworkStatusCode(statusCode) {
|
|
1897
|
+
return RETRYABLE_STATUS_CODES.has(statusCode);
|
|
1898
|
+
}
|
|
1899
|
+
function isRetryableNetworkError(error, options = {}) {
|
|
1900
|
+
return isRetryableNetworkErrorInternal(error, options, /* @__PURE__ */ new Set());
|
|
1901
|
+
}
|
|
1902
|
+
function isRetryableNetworkErrorInternal(error, options, seen) {
|
|
1903
|
+
if (error === void 0 || error === null) return options.treatUnknownAsRetryable ?? false;
|
|
1904
|
+
if (typeof error === "string") return isRetryableNetworkMessage(error);
|
|
1905
|
+
if (typeof error !== "object") return options.treatUnknownAsRetryable ?? false;
|
|
1906
|
+
if (seen.has(error)) return false;
|
|
1907
|
+
seen.add(error);
|
|
1908
|
+
const record = error;
|
|
1909
|
+
if (record.name === "AbortError") return false;
|
|
1910
|
+
const statusCode = readNumericField(record, ["statusCode", "status"]);
|
|
1911
|
+
if (statusCode !== void 0 && isRetryableNetworkStatusCode(statusCode)) return true;
|
|
1912
|
+
const code = typeof record.code === "string" ? record.code.toUpperCase() : void 0;
|
|
1913
|
+
if (code && RETRYABLE_ERROR_CODES.has(code)) return true;
|
|
1914
|
+
if (typeof record.message === "string" && isRetryableNetworkMessage(record.message)) return true;
|
|
1915
|
+
if ("cause" in record) return isRetryableNetworkErrorInternal(record.cause, options, seen);
|
|
1916
|
+
return options.treatUnknownAsRetryable ?? false;
|
|
1917
|
+
}
|
|
1918
|
+
function readNumericField(record, keys) {
|
|
1919
|
+
for (const key of keys) {
|
|
1920
|
+
const value = record[key];
|
|
1921
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
function isRetryableNetworkMessage(message) {
|
|
1925
|
+
const lower = message.toLowerCase();
|
|
1926
|
+
return RETRYABLE_MESSAGE_FRAGMENTS.some((fragment) => lower.includes(fragment)) || hasRetryableStatusInMessage(lower);
|
|
1927
|
+
}
|
|
1928
|
+
function hasRetryableStatusInMessage(message) {
|
|
1929
|
+
const statusMatch = message.match(/status\s+(\d{3})/);
|
|
1930
|
+
if (!statusMatch) return false;
|
|
1931
|
+
return isRetryableNetworkStatusCode(Number(statusMatch[1]));
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
//#endregion
|
|
1935
|
+
//#region src/translation-model-catalog.ts
|
|
1936
|
+
const DEFAULT_SEARCH_LIMIT = 6;
|
|
1937
|
+
const MAX_SEARCH_FETCH_LIMIT = 12;
|
|
1938
|
+
const DEFAULT_SMALL_VERIFY_MODEL_ID = "Xenova/opus-mt-no-de";
|
|
1939
|
+
const HUGGING_FACE_FETCH_RETRY_COUNT = 2;
|
|
1940
|
+
const HUGGING_FACE_FETCH_RETRY_DELAY_MS = 750;
|
|
1941
|
+
const HUGGING_FACE_FETCH_DISPATCHER = createProxyAwareDispatcher();
|
|
1942
|
+
async function searchLocalModels(input, options = {}) {
|
|
1943
|
+
const list = await fetchHuggingFaceModelList(input, options);
|
|
1944
|
+
return {
|
|
1945
|
+
items: rankCandidates(await Promise.all(list.items.map(async (item) => {
|
|
1946
|
+
const detail = await getHuggingFaceModelDetail(item.id, input, options).catch(() => null);
|
|
1947
|
+
return detail ? toTranslationModelCandidate(detail, input) : toTranslationModelCandidate(item, input);
|
|
1948
|
+
})), input).slice(0, normalizeSearchLimit(input.limit)),
|
|
1949
|
+
nextCursor: list.nextCursor
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
async function searchLocalModelsProgressively(input, options = {}) {
|
|
1953
|
+
const list = await fetchHuggingFaceModelList(input, options);
|
|
1954
|
+
const candidateShells = rankCandidates(list.items.map((item) => toTranslationModelCandidate(item, input)), input).slice(0, normalizeSearchLimit(input.limit));
|
|
1955
|
+
const events = [{
|
|
1956
|
+
requestId: input.requestId,
|
|
1957
|
+
phase: "candidates",
|
|
1958
|
+
items: candidateShells,
|
|
1959
|
+
nextCursor: list.nextCursor
|
|
1960
|
+
}];
|
|
1961
|
+
const enriched = await Promise.all(candidateShells.map(async (candidate) => {
|
|
1962
|
+
const detail = await getHuggingFaceModelDetail(candidate.id, input, options).catch(() => null);
|
|
1963
|
+
return detail ? toTranslationModelCandidate(detail, input) : candidate;
|
|
1964
|
+
}));
|
|
1965
|
+
events.push({
|
|
1966
|
+
requestId: input.requestId,
|
|
1967
|
+
phase: "enriched",
|
|
1968
|
+
items: rankCandidates(enriched, input).slice(0, normalizeSearchLimit(input.limit)),
|
|
1969
|
+
nextCursor: list.nextCursor
|
|
1970
|
+
});
|
|
1971
|
+
events.push({
|
|
1972
|
+
requestId: input.requestId,
|
|
1973
|
+
phase: "complete",
|
|
1974
|
+
items: rankCandidates(enriched, input).slice(0, normalizeSearchLimit(input.limit)),
|
|
1975
|
+
nextCursor: list.nextCursor
|
|
1976
|
+
});
|
|
1977
|
+
return events;
|
|
1978
|
+
}
|
|
1979
|
+
async function fetchHuggingFaceModelList(input, options) {
|
|
1980
|
+
const limit = normalizeSearchLimit(input.limit);
|
|
1981
|
+
const fetchLimit = Math.min(Math.max(limit * 2, limit), MAX_SEARCH_FETCH_LIMIT);
|
|
1982
|
+
const params = new URLSearchParams({
|
|
1983
|
+
pipeline_tag: "translation",
|
|
1984
|
+
sort: "trendingScore",
|
|
1985
|
+
direction: "-1",
|
|
1986
|
+
limit: String(fetchLimit)
|
|
1987
|
+
});
|
|
1988
|
+
if (input.query?.trim()) params.set("search", input.query.trim());
|
|
1989
|
+
if (input.cursor?.trim()) params.set("cursor", input.cursor.trim());
|
|
1990
|
+
const url = `${buildHuggingFaceApiBaseUrl(options.hfEndpoint)}/models?${params.toString()}`;
|
|
1991
|
+
const response = await fetchHuggingFace(url);
|
|
1992
|
+
const responseBody = await response.text();
|
|
1993
|
+
const fetchCacheStore = getFetchCacheStore(options);
|
|
1994
|
+
await fetchCacheStore.upsertProviderFetch({
|
|
1995
|
+
url,
|
|
1996
|
+
status: response.status,
|
|
1997
|
+
ok: response.ok,
|
|
1998
|
+
headers: headersToRecord(response.headers),
|
|
1999
|
+
bodyText: responseBody,
|
|
2000
|
+
queryContext: buildQueryContext(input)
|
|
2001
|
+
});
|
|
2002
|
+
if (!response.ok) throw new Error(`Hugging Face model search failed with status ${response.status}.`);
|
|
2003
|
+
const listJson = parseJson(responseBody);
|
|
2004
|
+
const rawItems = Array.isArray(listJson) ? listJson.filter(isRecord) : [];
|
|
2005
|
+
const items = rawItems.map(normalizeHfModelListItem).filter((item) => item !== null);
|
|
2006
|
+
for (const raw of rawItems) {
|
|
2007
|
+
const item = normalizeHfModelListItem(raw);
|
|
2008
|
+
if (!item) continue;
|
|
2009
|
+
await fetchCacheStore.upsertListItem({
|
|
2010
|
+
modelId: item.id,
|
|
2011
|
+
raw,
|
|
2012
|
+
queryContext: buildQueryContext(input)
|
|
2013
|
+
});
|
|
2014
|
+
}
|
|
2015
|
+
return {
|
|
2016
|
+
items,
|
|
2017
|
+
nextCursor: readNextCursor(response.headers.get("link"))
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
async function getHuggingFaceModelDetail(modelId, input, options) {
|
|
2021
|
+
const [namespace, repo] = modelId.split("/", 2);
|
|
2022
|
+
const modelPath = namespace && repo ? `${encodeURIComponent(namespace)}/${encodeURIComponent(repo)}` : encodeURIComponent(modelId);
|
|
2023
|
+
const url = `${buildHuggingFaceApiBaseUrl(options.hfEndpoint)}/models/${modelPath}?blobs=true`;
|
|
2024
|
+
const response = await fetchHuggingFace(url);
|
|
2025
|
+
const responseBody = await response.text();
|
|
2026
|
+
await getFetchCacheStore(options).upsertProviderFetch({
|
|
2027
|
+
url,
|
|
2028
|
+
status: response.status,
|
|
2029
|
+
ok: response.ok,
|
|
2030
|
+
headers: headersToRecord(response.headers),
|
|
2031
|
+
bodyText: responseBody,
|
|
2032
|
+
queryContext: input ? buildQueryContext(input) : void 0
|
|
2033
|
+
});
|
|
2034
|
+
if (!response.ok) throw new Error(`Hugging Face model detail failed with status ${response.status}.`);
|
|
2035
|
+
const detailJson = parseJson(responseBody);
|
|
2036
|
+
const raw = isRecord(detailJson) ? detailJson : {};
|
|
2037
|
+
await getFetchCacheStore(options).upsertDetail({
|
|
2038
|
+
modelId,
|
|
2039
|
+
raw,
|
|
2040
|
+
queryContext: input ? buildQueryContext(input) : void 0
|
|
2041
|
+
});
|
|
2042
|
+
return normalizeHfModelDetail(detailJson, modelId);
|
|
2043
|
+
}
|
|
2044
|
+
function parseJson(text) {
|
|
2045
|
+
try {
|
|
2046
|
+
return JSON.parse(text);
|
|
2047
|
+
} catch {
|
|
2048
|
+
return null;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
function headersToRecord(headers) {
|
|
2052
|
+
return Object.fromEntries(headers.entries());
|
|
2053
|
+
}
|
|
2054
|
+
async function fetchHuggingFace(input) {
|
|
2055
|
+
let lastError;
|
|
2056
|
+
for (let attempt = 0; attempt <= HUGGING_FACE_FETCH_RETRY_COUNT; attempt += 1) {
|
|
2057
|
+
try {
|
|
2058
|
+
const response = await fetchWithDispatcher(input);
|
|
2059
|
+
if (response.ok || !isRetryableNetworkStatusCode(response.status)) return response;
|
|
2060
|
+
lastError = /* @__PURE__ */ new Error(`Hugging Face request failed with status ${response.status}.`);
|
|
2061
|
+
if (attempt === HUGGING_FACE_FETCH_RETRY_COUNT) return response;
|
|
2062
|
+
await response.body?.cancel().catch(() => void 0);
|
|
2063
|
+
} catch (error) {
|
|
2064
|
+
lastError = error;
|
|
2065
|
+
if (!isRetryableFetchError(error) || attempt === HUGGING_FACE_FETCH_RETRY_COUNT) throw error;
|
|
2066
|
+
}
|
|
2067
|
+
await delay$1(HUGGING_FACE_FETCH_RETRY_DELAY_MS * (attempt + 1));
|
|
2068
|
+
}
|
|
2069
|
+
throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error("Hugging Face request failed.");
|
|
2070
|
+
}
|
|
2071
|
+
async function fetchWithDispatcher(input) {
|
|
2072
|
+
return fetch(input, { dispatcher: HUGGING_FACE_FETCH_DISPATCHER });
|
|
2073
|
+
}
|
|
2074
|
+
function isRetryableFetchError(error) {
|
|
2075
|
+
return isRetryableNetworkError(error);
|
|
2076
|
+
}
|
|
2077
|
+
function delay$1(ms) {
|
|
2078
|
+
return new Promise((resolve$1) => {
|
|
2079
|
+
setTimeout(resolve$1, ms);
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
function normalizeHfModelListItem(value) {
|
|
2083
|
+
if (!value || typeof value !== "object") return null;
|
|
2084
|
+
const record = value;
|
|
2085
|
+
if (typeof record.id !== "string" || record.id.length === 0) return null;
|
|
2086
|
+
return {
|
|
2087
|
+
id: record.id,
|
|
2088
|
+
pipeline_tag: typeof record.pipeline_tag === "string" ? record.pipeline_tag : void 0,
|
|
2089
|
+
tags: Array.isArray(record.tags) ? record.tags.filter(isString) : [],
|
|
2090
|
+
downloads: normalizeNonNegativeNumber(record.downloads),
|
|
2091
|
+
likes: normalizeNonNegativeNumber(record.likes),
|
|
2092
|
+
trendingScore: normalizeOptionalNumber(record.trendingScore),
|
|
2093
|
+
lastModified: typeof record.lastModified === "string" ? record.lastModified : void 0
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
function normalizeHfModelDetail(value, fallbackId) {
|
|
2097
|
+
const base = normalizeHfModelListItem(value) ?? {
|
|
2098
|
+
id: fallbackId,
|
|
2099
|
+
tags: [],
|
|
2100
|
+
downloads: 0,
|
|
2101
|
+
likes: 0
|
|
2102
|
+
};
|
|
2103
|
+
const record = value && typeof value === "object" ? value : {};
|
|
2104
|
+
const configRecord = record.config && typeof record.config === "object" ? record.config : void 0;
|
|
2105
|
+
return {
|
|
2106
|
+
...base,
|
|
2107
|
+
config: configRecord ? {
|
|
2108
|
+
is_encoder_decoder: typeof configRecord.is_encoder_decoder === "boolean" ? configRecord.is_encoder_decoder : void 0,
|
|
2109
|
+
model_type: typeof configRecord.model_type === "string" ? configRecord.model_type : void 0
|
|
2110
|
+
} : void 0,
|
|
2111
|
+
siblings: Array.isArray(record.siblings) ? record.siblings.map((entry) => normalizeSibling(entry)).filter((entry) => entry !== null) : []
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
function normalizeSibling(value) {
|
|
2115
|
+
if (!value || typeof value !== "object") return null;
|
|
2116
|
+
const record = value;
|
|
2117
|
+
if (typeof record.rfilename !== "string" || record.rfilename.length === 0) return null;
|
|
2118
|
+
return {
|
|
2119
|
+
rfilename: record.rfilename,
|
|
2120
|
+
size: normalizeOptionalNumber(record.size)
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
function toTranslationModelCandidate(detail, input) {
|
|
2124
|
+
const plan = isHfModelDetail(detail) ? resolveLocalModelPlan(detail) : null;
|
|
2125
|
+
const languageMatch = buildLanguageMatch(detail, input.sourceLanguage, input.targetLanguage);
|
|
2126
|
+
const compatibility = {
|
|
2127
|
+
transformersJs: detail.tags.includes("transformers.js"),
|
|
2128
|
+
onnx: detail.tags.includes("onnx"),
|
|
2129
|
+
localRuntimeVerified: detail.tags.includes("transformers.js") && detail.tags.includes("onnx")
|
|
2130
|
+
};
|
|
2131
|
+
const estimatedTotalBytes = plan?.estimatedTotalBytes;
|
|
2132
|
+
return {
|
|
2133
|
+
id: detail.id,
|
|
2134
|
+
label: detail.id,
|
|
2135
|
+
summary: buildCandidateSummary(detail, compatibility.localRuntimeVerified, estimatedTotalBytes),
|
|
2136
|
+
downloads: detail.downloads,
|
|
2137
|
+
likes: detail.likes,
|
|
2138
|
+
trendingScore: detail.trendingScore,
|
|
2139
|
+
lastModified: detail.lastModified,
|
|
2140
|
+
pipelineTag: detail.pipeline_tag,
|
|
2141
|
+
tags: detail.tags,
|
|
2142
|
+
compatibility,
|
|
2143
|
+
size: {
|
|
2144
|
+
estimatedTotalBytes,
|
|
2145
|
+
primaryBytes: estimatedTotalBytes
|
|
2146
|
+
},
|
|
2147
|
+
downloadGroups: plan?.groups,
|
|
2148
|
+
languageMatch
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
function resolveLocalModelPlan(detail) {
|
|
2152
|
+
return buildLocalDownloadPlanFromRepositoryFiles({
|
|
2153
|
+
modelId: detail.id,
|
|
2154
|
+
isEncoderDecoder: detail.config?.is_encoder_decoder,
|
|
2155
|
+
files: detail.siblings.map((entry) => ({
|
|
2156
|
+
path: entry.rfilename,
|
|
2157
|
+
sizeBytes: entry.size
|
|
2158
|
+
}))
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
function buildCandidateSummary(detail, verified, estimatedTotalBytes) {
|
|
2162
|
+
const parts = [verified ? "Verified Transformers.js + ONNX model." : "Translation model from Hugging Face."];
|
|
2163
|
+
if (estimatedTotalBytes !== void 0) parts.push(`Estimated download ${formatBytes$1(estimatedTotalBytes)}.`);
|
|
2164
|
+
if (detail.tags.includes("translation")) parts.push("Tagged for translation.");
|
|
2165
|
+
return parts.join(" ");
|
|
2166
|
+
}
|
|
2167
|
+
function buildLanguageMatch(detail, sourceLanguage, targetLanguage) {
|
|
2168
|
+
const sourceTokens = buildLanguageTokens(sourceLanguage);
|
|
2169
|
+
const targetTokens = buildLanguageTokens(targetLanguage);
|
|
2170
|
+
const searchable = [detail.id, ...detail.tags].join(" ").toLowerCase();
|
|
2171
|
+
const sourceMatched = sourceTokens.some((token) => searchable.includes(token));
|
|
2172
|
+
const targetMatched = targetTokens.some((token) => searchable.includes(token));
|
|
2173
|
+
let directionalScore = 0;
|
|
2174
|
+
if (sourceTokens.length > 0 && targetTokens.length > 0) {
|
|
2175
|
+
for (const sourceToken of sourceTokens) for (const targetToken of targetTokens) if (searchable.includes(`${sourceToken}-${targetToken}`) || searchable.includes(`${sourceToken}_${targetToken}`) || searchable.includes(`-${sourceToken}-${targetToken}`)) directionalScore = Math.max(directionalScore, 2);
|
|
2176
|
+
}
|
|
2177
|
+
if (targetMatched) directionalScore += 1;
|
|
2178
|
+
if (sourceMatched) directionalScore += .5;
|
|
2179
|
+
return {
|
|
2180
|
+
sourceMatched,
|
|
2181
|
+
targetMatched,
|
|
2182
|
+
directionalScore
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
function buildLanguageTokens(language) {
|
|
2186
|
+
if (!language) return [];
|
|
2187
|
+
const normalized = language.trim().toLowerCase();
|
|
2188
|
+
if (!normalized) return [];
|
|
2189
|
+
const primary = normalized.split(/[-_]/, 1)[0] ?? normalized;
|
|
2190
|
+
return [...new Set([normalized, primary].filter(Boolean))];
|
|
2191
|
+
}
|
|
2192
|
+
function rankCandidate(candidate) {
|
|
2193
|
+
const smallVerifyBoost = candidate.id === DEFAULT_SMALL_VERIFY_MODEL_ID ? 18 : 0;
|
|
2194
|
+
const compatibilityBoost = candidate.compatibility.localRuntimeVerified ? 140 : -80;
|
|
2195
|
+
const trend = (candidate.trendingScore ?? 0) * 100;
|
|
2196
|
+
const directionalBoost = candidate.languageMatch.directionalScore * 45;
|
|
2197
|
+
const downloadsBoost = Math.log10(candidate.downloads + 1) * 12;
|
|
2198
|
+
const likesBoost = candidate.likes * .15;
|
|
2199
|
+
const sizePenalty = candidate.size.estimatedTotalBytes === void 0 ? 0 : Math.log10(candidate.size.estimatedTotalBytes / (1024 * 1024) + 1) * 12;
|
|
2200
|
+
return smallVerifyBoost + compatibilityBoost + trend + directionalBoost + downloadsBoost + likesBoost - sizePenalty;
|
|
2201
|
+
}
|
|
2202
|
+
function rankCandidates(candidates, input) {
|
|
2203
|
+
const verifiedCandidates = candidates.filter((candidate) => candidate.compatibility.localRuntimeVerified);
|
|
2204
|
+
return [...input.query?.trim() ? candidates : verifiedCandidates].sort((left, right) => rankCandidate(right) - rankCandidate(left));
|
|
2205
|
+
}
|
|
2206
|
+
function normalizeSearchLimit(limit) {
|
|
2207
|
+
return Math.min(Math.max(limit ?? DEFAULT_SEARCH_LIMIT, 1), 20);
|
|
2208
|
+
}
|
|
2209
|
+
function buildQueryContext(input) {
|
|
2210
|
+
return {
|
|
2211
|
+
...input.query?.trim() ? { query: input.query.trim() } : {},
|
|
2212
|
+
...input.sourceLanguage?.trim() ? { sourceLanguage: input.sourceLanguage.trim() } : {},
|
|
2213
|
+
...input.targetLanguage?.trim() ? { targetLanguage: input.targetLanguage.trim() } : {}
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
function getFetchCacheStore(options) {
|
|
2217
|
+
return options.fetchCacheStore ?? new LocalModelFetchCacheStore({ cachePath: getDefaultLocalModelFetchCachePath() });
|
|
2218
|
+
}
|
|
2219
|
+
function readNextCursor(linkHeader) {
|
|
2220
|
+
if (!linkHeader) return void 0;
|
|
2221
|
+
const match = /[?&]cursor=([^&>]+).*rel="next"/.exec(linkHeader);
|
|
2222
|
+
if (!match) return void 0;
|
|
2223
|
+
try {
|
|
2224
|
+
return decodeURIComponent(match[1]);
|
|
2225
|
+
} catch {
|
|
2226
|
+
return match[1];
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
function formatBytes$1(value) {
|
|
2230
|
+
if (!Number.isFinite(value) || value <= 0) return "0 B";
|
|
2231
|
+
const units = [
|
|
2232
|
+
"B",
|
|
2233
|
+
"KB",
|
|
2234
|
+
"MB",
|
|
2235
|
+
"GB"
|
|
2236
|
+
];
|
|
2237
|
+
let size = value;
|
|
2238
|
+
let unitIndex = 0;
|
|
2239
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
2240
|
+
size /= 1024;
|
|
2241
|
+
unitIndex += 1;
|
|
2242
|
+
}
|
|
2243
|
+
const digits = size >= 100 || unitIndex === 0 ? 0 : 1;
|
|
2244
|
+
return `${size.toFixed(digits)} ${units[unitIndex]}`;
|
|
2245
|
+
}
|
|
2246
|
+
function normalizeNonNegativeNumber(value) {
|
|
2247
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
2248
|
+
}
|
|
2249
|
+
function normalizeOptionalNumber(value) {
|
|
2250
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
2251
|
+
}
|
|
2252
|
+
function isHfModelDetail(value) {
|
|
2253
|
+
return "siblings" in value;
|
|
2254
|
+
}
|
|
2255
|
+
function isString(value) {
|
|
2256
|
+
return typeof value === "string";
|
|
2257
|
+
}
|
|
2258
|
+
function isRecord(value) {
|
|
2259
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
//#endregion
|
|
2263
|
+
//#region src/local-model-asset-service.ts
|
|
2264
|
+
const DEFAULT_NETWORK_RETRY_LIMIT = Number.POSITIVE_INFINITY;
|
|
2265
|
+
const DEFAULT_NETWORK_RETRY_DELAY_MS = 500;
|
|
2266
|
+
const DEFAULT_NETWORK_RETRY_DELAY_MAX_MS = 5e3;
|
|
2267
|
+
var LocalModelAssetService = class {
|
|
2268
|
+
now;
|
|
2269
|
+
store;
|
|
2270
|
+
cacheDir;
|
|
2271
|
+
fetchCacheStore;
|
|
2272
|
+
networkRetryPolicy;
|
|
2273
|
+
listeners = /* @__PURE__ */ new Set();
|
|
2274
|
+
sessions = /* @__PURE__ */ new Map();
|
|
2275
|
+
sessionTasks = /* @__PURE__ */ new Map();
|
|
2276
|
+
logs = /* @__PURE__ */ new Map();
|
|
2277
|
+
transformersModulePromise = null;
|
|
2278
|
+
constructor(options) {
|
|
2279
|
+
this.options = options;
|
|
2280
|
+
ensureProxyAwareFetchDispatcher();
|
|
2281
|
+
this.now = options.now ?? Date.now;
|
|
2282
|
+
this.cacheDir = options.cacheDir ?? getDefaultLocalModelCacheDir();
|
|
2283
|
+
this.networkRetryPolicy = {
|
|
2284
|
+
limit: options.networkRetryPolicy?.limit ?? DEFAULT_NETWORK_RETRY_LIMIT,
|
|
2285
|
+
delayMs: options.networkRetryPolicy?.delayMs ?? DEFAULT_NETWORK_RETRY_DELAY_MS,
|
|
2286
|
+
maxDelayMs: options.networkRetryPolicy?.maxDelayMs ?? DEFAULT_NETWORK_RETRY_DELAY_MAX_MS
|
|
2287
|
+
};
|
|
2288
|
+
this.store = new LocalModelAssetStore({ indexPath: options.indexPath ?? getDefaultLocalModelIndexPath() });
|
|
2289
|
+
this.fetchCacheStore = new LocalModelFetchCacheStore({
|
|
2290
|
+
cachePath: options.fetchCachePath ?? getDefaultLocalModelFetchCachePath(),
|
|
2291
|
+
now: this.now
|
|
2292
|
+
});
|
|
2293
|
+
}
|
|
2294
|
+
subscribeLogs() {
|
|
2295
|
+
return observable((emit) => {
|
|
2296
|
+
for (const log of this.logs.values()) emit.next(log);
|
|
2297
|
+
const listener = (log) => emit.next(log);
|
|
2298
|
+
this.listeners.add(listener);
|
|
2299
|
+
return () => {
|
|
2300
|
+
this.listeners.delete(listener);
|
|
2301
|
+
};
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
2304
|
+
async listLocalCatalog() {
|
|
2305
|
+
const localMap = await this.store.readMap();
|
|
2306
|
+
const items = await Promise.all([...localMap.values()].map(async (state) => {
|
|
2307
|
+
const asset = await this.refreshCachedState(state);
|
|
2308
|
+
return toCatalogItem({
|
|
2309
|
+
id: state.modelId,
|
|
2310
|
+
label: state.modelId,
|
|
2311
|
+
summary: state.plan?.estimatedTotalBytes !== void 0 ? `Previously selected local model. Estimated download ${formatBytes(state.plan.estimatedTotalBytes)}.` : "Previously selected local model.",
|
|
2312
|
+
downloads: 0,
|
|
2313
|
+
likes: 0,
|
|
2314
|
+
tags: ["local"],
|
|
2315
|
+
compatibility: {
|
|
2316
|
+
transformersJs: true,
|
|
2317
|
+
onnx: true,
|
|
2318
|
+
localRuntimeVerified: true
|
|
2319
|
+
},
|
|
2320
|
+
size: {
|
|
2321
|
+
estimatedTotalBytes: state.plan?.estimatedTotalBytes,
|
|
2322
|
+
primaryBytes: state.plan?.estimatedTotalBytes
|
|
2323
|
+
},
|
|
2324
|
+
downloadGroups: state.plan?.groups,
|
|
2325
|
+
languageMatch: {
|
|
2326
|
+
sourceMatched: false,
|
|
2327
|
+
targetMatched: false,
|
|
2328
|
+
directionalScore: 0
|
|
2329
|
+
}
|
|
2330
|
+
}, asset);
|
|
2331
|
+
}));
|
|
2332
|
+
items.sort(compareCatalogItems);
|
|
2333
|
+
return { items };
|
|
2334
|
+
}
|
|
2335
|
+
async searchRemoteCatalog(input) {
|
|
2336
|
+
const [remote, localMap, selectedModel] = await Promise.all([
|
|
2337
|
+
this.searchRemote(input),
|
|
2338
|
+
this.store.readMap(),
|
|
2339
|
+
this.readSelectedModel()
|
|
2340
|
+
]);
|
|
2341
|
+
const items = await this.decorateCatalogItems(remote.items, localMap, selectedModel);
|
|
2342
|
+
items.sort(compareCatalogItems);
|
|
2343
|
+
return {
|
|
2344
|
+
items,
|
|
2345
|
+
nextCursor: remote.nextCursor
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
subscribeRemoteCatalog(input) {
|
|
2349
|
+
return observable((emit) => {
|
|
2350
|
+
let active = true;
|
|
2351
|
+
(async () => {
|
|
2352
|
+
try {
|
|
2353
|
+
const events = await searchLocalModelsProgressively(input, {
|
|
2354
|
+
fetchCacheStore: this.fetchCacheStore,
|
|
2355
|
+
hfEndpoint: await this.readHuggingFaceEndpoint()
|
|
2356
|
+
});
|
|
2357
|
+
for (const event of events) {
|
|
2358
|
+
if (!active) return;
|
|
2359
|
+
const localMap = await this.store.readMap();
|
|
2360
|
+
const selectedModel = await this.readSelectedModel();
|
|
2361
|
+
const items = event.items ? await this.decorateCatalogItems(event.items, localMap, selectedModel, { includeLocalOnly: false }) : void 0;
|
|
2362
|
+
emit.next({
|
|
2363
|
+
requestId: event.requestId,
|
|
2364
|
+
phase: event.phase,
|
|
2365
|
+
items,
|
|
2366
|
+
nextCursor: event.nextCursor,
|
|
2367
|
+
message: event.message
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
} catch (error) {
|
|
2371
|
+
if (!active) return;
|
|
2372
|
+
emit.next({
|
|
2373
|
+
requestId: input.requestId,
|
|
2374
|
+
phase: "error",
|
|
2375
|
+
message: error instanceof Error ? error.message : "Unable to search remote local models."
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
})();
|
|
2379
|
+
return () => {
|
|
2380
|
+
active = false;
|
|
2381
|
+
};
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
async listCatalog(input) {
|
|
2385
|
+
return this.searchRemoteCatalog(input);
|
|
2386
|
+
}
|
|
2387
|
+
async readSelectedModelState(modelId, selectedGroupId) {
|
|
2388
|
+
const state = (await this.store.readMap()).get(modelId);
|
|
2389
|
+
if (state) return this.refreshCachedState(state, selectedGroupId);
|
|
2390
|
+
const session = this.sessions.get(modelId);
|
|
2391
|
+
if (session) {
|
|
2392
|
+
const selected = modelId === await this.readSelectedModel();
|
|
2393
|
+
const plan = await this.readPlanForState(modelId, selectedGroupId ?? session.selectedGroupId);
|
|
2394
|
+
const selectedGroup = selectLocalDownloadGroup(plan, selectedGroupId ?? session.selectedGroupId);
|
|
2395
|
+
const files = (selectedGroup?.files ?? plan?.files ?? []).map((file) => ({
|
|
2396
|
+
path: file.path,
|
|
2397
|
+
sizeBytes: file.sizeBytes,
|
|
2398
|
+
downloadedBytes: 0
|
|
2399
|
+
}));
|
|
2400
|
+
return LocalModelAssetStateSchema.parse({
|
|
2401
|
+
modelId,
|
|
2402
|
+
plan: plan ?? void 0,
|
|
2403
|
+
status: "downloading",
|
|
2404
|
+
selected,
|
|
2405
|
+
resumable: true,
|
|
2406
|
+
totalBytes: selectedGroup?.estimatedTotalBytes ?? plan?.estimatedTotalBytes,
|
|
2407
|
+
progress: 0,
|
|
2408
|
+
files,
|
|
2409
|
+
updatedAt: this.now()
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2412
|
+
return LocalModelAssetStateSchema.parse({
|
|
2413
|
+
modelId,
|
|
2414
|
+
status: "not-downloaded",
|
|
2415
|
+
selected: modelId === await this.readSelectedModel(),
|
|
2416
|
+
updatedAt: this.now()
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
async startDownload(modelId, selectedGroupId) {
|
|
2420
|
+
return this.runDownload(modelId, "downloading", "Downloading local model", selectedGroupId);
|
|
2421
|
+
}
|
|
2422
|
+
async resumeDownload(modelId, selectedGroupId) {
|
|
2423
|
+
return this.runDownload(modelId, "downloading", "Resuming local model download", selectedGroupId);
|
|
2424
|
+
}
|
|
2425
|
+
async pauseDownload(modelId) {
|
|
2426
|
+
const session = this.sessions.get(modelId);
|
|
2427
|
+
if (session) {
|
|
2428
|
+
session.abortController.abort();
|
|
2429
|
+
this.sessions.delete(modelId);
|
|
2430
|
+
}
|
|
2431
|
+
const current = await this.readSelectedModelState(modelId);
|
|
2432
|
+
const nextState = LocalModelAssetStateSchema.parse({
|
|
2433
|
+
...current,
|
|
2434
|
+
status: "paused",
|
|
2435
|
+
resumable: true,
|
|
2436
|
+
updatedAt: this.now()
|
|
2437
|
+
});
|
|
2438
|
+
await this.store.upsert(nextState);
|
|
2439
|
+
this.emitLog({
|
|
2440
|
+
engineId: "local",
|
|
2441
|
+
modelId,
|
|
2442
|
+
selectedGroupId: current.plan?.selectedGroupId,
|
|
2443
|
+
status: "paused",
|
|
2444
|
+
message: "Local model download paused.",
|
|
2445
|
+
progress: nextState.progress,
|
|
2446
|
+
bytesDownloaded: nextState.bytesDownloaded,
|
|
2447
|
+
totalBytes: nextState.totalBytes,
|
|
2448
|
+
resumable: true,
|
|
2449
|
+
files: nextState.files,
|
|
2450
|
+
updatedAt: this.now()
|
|
2451
|
+
});
|
|
2452
|
+
return { success: true };
|
|
2453
|
+
}
|
|
2454
|
+
async deleteModel(modelId) {
|
|
2455
|
+
this.sessions.get(modelId)?.abortController.abort();
|
|
2456
|
+
this.sessions.delete(modelId);
|
|
2457
|
+
const current = await this.readSelectedModelState(modelId);
|
|
2458
|
+
await this.store.upsert(LocalModelAssetStateSchema.parse({
|
|
2459
|
+
...current,
|
|
2460
|
+
status: "deleting",
|
|
2461
|
+
updatedAt: this.now()
|
|
2462
|
+
}));
|
|
2463
|
+
this.emitLog({
|
|
2464
|
+
engineId: "local",
|
|
2465
|
+
modelId,
|
|
2466
|
+
selectedGroupId: current.plan?.selectedGroupId,
|
|
2467
|
+
status: "deleting",
|
|
2468
|
+
message: "Deleting local model files.",
|
|
2469
|
+
files: current.files,
|
|
2470
|
+
updatedAt: this.now()
|
|
2471
|
+
});
|
|
2472
|
+
await mkdir(this.cacheDir, { recursive: true });
|
|
2473
|
+
await rm(getTransformersLocalModelPath(this.cacheDir, modelId), {
|
|
2474
|
+
recursive: true,
|
|
2475
|
+
force: true
|
|
2476
|
+
});
|
|
2477
|
+
await rm(getTransformersFileCacheModelPath(this.cacheDir, modelId), {
|
|
2478
|
+
recursive: true,
|
|
2479
|
+
force: true
|
|
2480
|
+
});
|
|
2481
|
+
await rm(getHubCacheRepoPath(this.cacheDir, modelId), {
|
|
2482
|
+
recursive: true,
|
|
2483
|
+
force: true
|
|
2484
|
+
});
|
|
2485
|
+
await this.store.remove(modelId);
|
|
2486
|
+
this.emitLog({
|
|
2487
|
+
engineId: "local",
|
|
2488
|
+
modelId,
|
|
2489
|
+
selectedGroupId: current.plan?.selectedGroupId,
|
|
2490
|
+
status: "not-downloaded",
|
|
2491
|
+
message: "Local model files were removed.",
|
|
2492
|
+
progress: 0,
|
|
2493
|
+
bytesDownloaded: 0,
|
|
2494
|
+
totalBytes: 0,
|
|
2495
|
+
files: [],
|
|
2496
|
+
updatedAt: this.now()
|
|
2497
|
+
});
|
|
2498
|
+
return { success: true };
|
|
2499
|
+
}
|
|
2500
|
+
async markSelectedModel(modelId) {
|
|
2501
|
+
const nextStates = (await this.store.readAll()).map((state) => LocalModelAssetStateSchema.parse({
|
|
2502
|
+
...state,
|
|
2503
|
+
selected: state.modelId === modelId
|
|
2504
|
+
}));
|
|
2505
|
+
if (!nextStates.some((state) => state.modelId === modelId)) nextStates.push(LocalModelAssetStateSchema.parse({
|
|
2506
|
+
modelId,
|
|
2507
|
+
status: "not-downloaded",
|
|
2508
|
+
selected: true,
|
|
2509
|
+
updatedAt: this.now()
|
|
2510
|
+
}));
|
|
2511
|
+
await this.store.writeAll(nextStates);
|
|
2512
|
+
}
|
|
2513
|
+
async waitForModelTask(modelId) {
|
|
2514
|
+
await this.sessionTasks.get(modelId);
|
|
2515
|
+
}
|
|
2516
|
+
async close() {
|
|
2517
|
+
const sessions = [...this.sessions.values()];
|
|
2518
|
+
for (const session of sessions) session.abortController.abort();
|
|
2519
|
+
await Promise.allSettled(this.sessionTasks.values());
|
|
2520
|
+
}
|
|
2521
|
+
async searchRemote(input) {
|
|
2522
|
+
return searchLocalModels({
|
|
2523
|
+
query: input.query,
|
|
2524
|
+
sourceLanguage: input.sourceLanguage,
|
|
2525
|
+
targetLanguage: input.targetLanguage,
|
|
2526
|
+
limit: input.limit,
|
|
2527
|
+
cursor: input.cursor
|
|
2528
|
+
}, {
|
|
2529
|
+
fetchCacheStore: this.fetchCacheStore,
|
|
2530
|
+
hfEndpoint: await this.readHuggingFaceEndpoint()
|
|
2531
|
+
});
|
|
2532
|
+
}
|
|
2533
|
+
async decorateCatalogItems(candidates, localMap, selectedModel, options = {}) {
|
|
2534
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2535
|
+
const remoteItems = await Promise.all(candidates.map(async (candidate) => {
|
|
2536
|
+
seen.add(candidate.id);
|
|
2537
|
+
const localState = localMap.get(candidate.id);
|
|
2538
|
+
return toCatalogItem(candidate, localState ? await this.refreshCachedState(localState) : LocalModelAssetStateSchema.parse({
|
|
2539
|
+
modelId: candidate.id,
|
|
2540
|
+
status: "not-downloaded",
|
|
2541
|
+
selected: candidate.id === selectedModel,
|
|
2542
|
+
updatedAt: this.now()
|
|
2543
|
+
}));
|
|
2544
|
+
}));
|
|
2545
|
+
const localOnlyItems = options.includeLocalOnly === false ? [] : await Promise.all([...localMap.values()].filter((state) => !seen.has(state.modelId)).map(async (state) => {
|
|
2546
|
+
const asset = await this.refreshCachedState(state);
|
|
2547
|
+
return toCatalogItem({
|
|
2548
|
+
id: state.modelId,
|
|
2549
|
+
label: state.modelId,
|
|
2550
|
+
summary: state.plan?.estimatedTotalBytes !== void 0 ? `Previously selected local model. Estimated download ${formatBytes(state.plan.estimatedTotalBytes)}.` : "Previously selected local model.",
|
|
2551
|
+
downloads: 0,
|
|
2552
|
+
likes: 0,
|
|
2553
|
+
tags: ["local"],
|
|
2554
|
+
compatibility: {
|
|
2555
|
+
transformersJs: true,
|
|
2556
|
+
onnx: true,
|
|
2557
|
+
localRuntimeVerified: true
|
|
2558
|
+
},
|
|
2559
|
+
size: {
|
|
2560
|
+
estimatedTotalBytes: state.plan?.estimatedTotalBytes,
|
|
2561
|
+
primaryBytes: state.plan?.estimatedTotalBytes
|
|
2562
|
+
},
|
|
2563
|
+
downloadGroups: state.plan?.groups,
|
|
2564
|
+
languageMatch: {
|
|
2565
|
+
sourceMatched: false,
|
|
2566
|
+
targetMatched: false,
|
|
2567
|
+
directionalScore: 0
|
|
2568
|
+
}
|
|
2569
|
+
}, asset);
|
|
2570
|
+
}));
|
|
2571
|
+
return [...remoteItems, ...localOnlyItems];
|
|
2572
|
+
}
|
|
2573
|
+
async refreshCachedState(state, selectedGroupId) {
|
|
2574
|
+
const requestedGroupId = selectedGroupId;
|
|
2575
|
+
const session = this.sessions.get(state.modelId);
|
|
2576
|
+
const selected = state.selected || state.modelId === await this.readSelectedModel();
|
|
2577
|
+
if (state.status === "deleting") return LocalModelAssetStateSchema.parse({
|
|
2578
|
+
...state,
|
|
2579
|
+
selected,
|
|
2580
|
+
updatedAt: this.now()
|
|
2581
|
+
});
|
|
2582
|
+
if (state.status === "downloaded" && state.plan && (requestedGroupId === void 0 || requestedGroupId === state.plan.selectedGroupId)) {
|
|
2583
|
+
const files$1 = (selectLocalDownloadGroup(state.plan, requestedGroupId ?? state.plan.selectedGroupId)?.files ?? state.plan.files).map((file) => ({
|
|
2584
|
+
path: file.path,
|
|
2585
|
+
sizeBytes: file.sizeBytes,
|
|
2586
|
+
downloadedBytes: file.sizeBytes
|
|
2587
|
+
}));
|
|
2588
|
+
return LocalModelAssetStateSchema.parse({
|
|
2589
|
+
...state,
|
|
2590
|
+
selected,
|
|
2591
|
+
status: "downloaded",
|
|
2592
|
+
progress: 1,
|
|
2593
|
+
bytesDownloaded: state.totalBytes ?? state.plan.estimatedTotalBytes,
|
|
2594
|
+
totalBytes: state.totalBytes ?? state.plan.estimatedTotalBytes,
|
|
2595
|
+
resumable: false,
|
|
2596
|
+
error: void 0,
|
|
2597
|
+
files: files$1,
|
|
2598
|
+
updatedAt: this.now(),
|
|
2599
|
+
installedAt: state.installedAt ?? this.now()
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
const transformers = await this.getTransformersModule();
|
|
2603
|
+
transformers.env.remoteHost = buildTransformersRemoteHost(await this.readHuggingFaceEndpoint());
|
|
2604
|
+
const [plan, persistedSelectedGroupId] = await Promise.all([this.readPlan(state.modelId, transformers, requestedGroupId ?? state.plan?.selectedGroupId), this.readSelectedGroupId()]);
|
|
2605
|
+
if (!plan && state.status !== "downloaded") return LocalModelAssetStateSchema.parse({
|
|
2606
|
+
...state,
|
|
2607
|
+
selected,
|
|
2608
|
+
plan: void 0
|
|
2609
|
+
});
|
|
2610
|
+
const effectivePlan = plan ?? {
|
|
2611
|
+
modelId: state.modelId,
|
|
2612
|
+
estimatedTotalBytes: state.totalBytes,
|
|
2613
|
+
files: state.files.map((file) => ({
|
|
2614
|
+
path: file.path,
|
|
2615
|
+
sizeBytes: file.sizeBytes,
|
|
2616
|
+
required: true
|
|
2617
|
+
})),
|
|
2618
|
+
selectedGroupId: state.plan?.selectedGroupId,
|
|
2619
|
+
groups: state.plan?.groups
|
|
2620
|
+
};
|
|
2621
|
+
const selectedGroup = selectLocalDownloadGroup(effectivePlan, requestedGroupId ?? state.plan?.selectedGroupId ?? persistedSelectedGroupId);
|
|
2622
|
+
if (requestedGroupId && requestedGroupId !== effectivePlan.selectedGroupId && !selectedGroup) return LocalModelAssetStateSchema.parse({
|
|
2623
|
+
...state,
|
|
2624
|
+
selected,
|
|
2625
|
+
status: "not-downloaded",
|
|
2626
|
+
progress: 0,
|
|
2627
|
+
bytesDownloaded: 0,
|
|
2628
|
+
totalBytes: void 0,
|
|
2629
|
+
resumable: false,
|
|
2630
|
+
files: [],
|
|
2631
|
+
updatedAt: this.now()
|
|
2632
|
+
});
|
|
2633
|
+
const planFiles = selectedGroup?.files ?? effectivePlan.files;
|
|
2634
|
+
const cacheStatus = await readLocalModelFileStatus({
|
|
2635
|
+
cacheDir: this.cacheDir,
|
|
2636
|
+
modelId: state.modelId,
|
|
2637
|
+
files: planFiles.map((file) => file.path)
|
|
2638
|
+
});
|
|
2639
|
+
const sameRequestedGroup = requestedGroupId === void 0 || requestedGroupId === state.plan?.selectedGroupId;
|
|
2640
|
+
const cachedFileSet = new Set(cacheStatus.files.filter((file) => file.cached).map((file) => file.file));
|
|
2641
|
+
const runtimeAllCached = cacheStatus.allCached;
|
|
2642
|
+
const files = planFiles.map((file) => {
|
|
2643
|
+
const cached = cachedFileSet.has(file.path);
|
|
2644
|
+
const existingDownloadedBytes = state.files.find((entry) => entry.path === file.path)?.downloadedBytes ?? 0;
|
|
2645
|
+
const downloadedBytes = file.sizeBytes === void 0 ? cached ? existingDownloadedBytes : existingDownloadedBytes : cached ? file.sizeBytes : Math.min(existingDownloadedBytes, file.sizeBytes);
|
|
2646
|
+
return {
|
|
2647
|
+
path: file.path,
|
|
2648
|
+
sizeBytes: file.sizeBytes,
|
|
2649
|
+
downloadedBytes
|
|
2650
|
+
};
|
|
2651
|
+
});
|
|
2652
|
+
const detectedBytesDownloaded = sumDownloadedBytes(files);
|
|
2653
|
+
const detectedProgress = effectivePlan.estimatedTotalBytes !== void 0 && effectivePlan.estimatedTotalBytes > 0 ? detectedBytesDownloaded / effectivePlan.estimatedTotalBytes : runtimeAllCached ? 1 : void 0;
|
|
2654
|
+
const progress = cacheStatus.allCached ? 1 : session ? state.progress : detectedProgress;
|
|
2655
|
+
const hasPartialCache = !runtimeAllCached && detectedBytesDownloaded > 0;
|
|
2656
|
+
return LocalModelAssetStateSchema.parse({
|
|
2657
|
+
...state,
|
|
2658
|
+
selected,
|
|
2659
|
+
plan: effectivePlan,
|
|
2660
|
+
status: runtimeAllCached ? "downloaded" : session ? state.status : sameRequestedGroup && state.status === "paused" ? "paused" : sameRequestedGroup && state.status === "error" ? "error" : hasPartialCache ? "paused" : "not-downloaded",
|
|
2661
|
+
progress: progress === void 0 ? void 0 : Math.max(0, Math.min(1, progress)),
|
|
2662
|
+
totalBytes: effectivePlan.estimatedTotalBytes ?? state.totalBytes,
|
|
2663
|
+
bytesDownloaded: session ? state.bytesDownloaded : detectedBytesDownloaded,
|
|
2664
|
+
error: runtimeAllCached ? void 0 : state.error,
|
|
2665
|
+
resumable: runtimeAllCached ? false : sameRequestedGroup && state.status === "paused" || sameRequestedGroup && state.status === "error" || hasPartialCache || progress !== void 0 && progress > 0 && progress < 1,
|
|
2666
|
+
files,
|
|
2667
|
+
updatedAt: this.now(),
|
|
2668
|
+
installedAt: runtimeAllCached ? state.installedAt ?? this.now() : state.installedAt
|
|
2669
|
+
});
|
|
2670
|
+
}
|
|
2671
|
+
async runDownload(modelId, targetStatus, messagePrefix, selectedGroupId) {
|
|
2672
|
+
const existing = this.sessions.get(modelId);
|
|
2673
|
+
if (existing) return { sessionId: existing.sessionId };
|
|
2674
|
+
const sessionId = `local-model-${sanitizeId(modelId)}-${this.now()}`;
|
|
2675
|
+
const abortController = new AbortController();
|
|
2676
|
+
this.sessions.set(modelId, {
|
|
2677
|
+
modelId,
|
|
2678
|
+
sessionId,
|
|
2679
|
+
abortController,
|
|
2680
|
+
selectedGroupId
|
|
2681
|
+
});
|
|
2682
|
+
const current = await this.readSelectedModelState(modelId);
|
|
2683
|
+
const transformers = await this.getTransformersModule();
|
|
2684
|
+
const plan = await this.readPlan(modelId, transformers, selectedGroupId ?? current.plan?.selectedGroupId);
|
|
2685
|
+
if (!plan || plan.files.length === 0 || plan.estimatedTotalBytes === void 0) {
|
|
2686
|
+
this.sessions.delete(modelId);
|
|
2687
|
+
throw new Error("No concrete local model download plan is available.");
|
|
2688
|
+
}
|
|
2689
|
+
const totalBytes = plan.estimatedTotalBytes;
|
|
2690
|
+
const resumedFiles = buildDownloadStateFiles({
|
|
2691
|
+
planFiles: plan.files,
|
|
2692
|
+
currentFiles: current.files
|
|
2693
|
+
});
|
|
2694
|
+
const resumedBytesDownloaded = sumDownloadedBytes(resumedFiles);
|
|
2695
|
+
const nextState = LocalModelAssetStateSchema.parse({
|
|
2696
|
+
...current,
|
|
2697
|
+
modelId,
|
|
2698
|
+
plan,
|
|
2699
|
+
status: targetStatus,
|
|
2700
|
+
selected: true,
|
|
2701
|
+
bytesDownloaded: resumedBytesDownloaded,
|
|
2702
|
+
progress: totalBytes > 0 ? resumedBytesDownloaded / totalBytes : current.progress,
|
|
2703
|
+
totalBytes,
|
|
2704
|
+
resumable: true,
|
|
2705
|
+
files: resumedFiles,
|
|
2706
|
+
updatedAt: this.now()
|
|
2707
|
+
});
|
|
2708
|
+
await this.store.upsert(nextState);
|
|
2709
|
+
this.emitLog({
|
|
2710
|
+
engineId: "local",
|
|
2711
|
+
modelId,
|
|
2712
|
+
selectedGroupId: nextState.plan?.selectedGroupId,
|
|
2713
|
+
status: targetStatus,
|
|
2714
|
+
message: `${messagePrefix} ${modelId}.`,
|
|
2715
|
+
progress: nextState.progress,
|
|
2716
|
+
bytesDownloaded: nextState.bytesDownloaded,
|
|
2717
|
+
totalBytes,
|
|
2718
|
+
sessionId,
|
|
2719
|
+
resumable: true,
|
|
2720
|
+
files: nextState.files,
|
|
2721
|
+
updatedAt: this.now()
|
|
2722
|
+
});
|
|
2723
|
+
const task = this.performDownload(modelId, sessionId, abortController.signal, nextState).catch((error) => this.finishDownload(modelId, sessionId, false, error instanceof Error ? error.message : String(error))).finally(() => {
|
|
2724
|
+
if (this.sessionTasks.get(modelId) === task) this.sessionTasks.delete(modelId);
|
|
2725
|
+
});
|
|
2726
|
+
this.sessionTasks.set(modelId, task);
|
|
2727
|
+
return { sessionId };
|
|
2728
|
+
}
|
|
2729
|
+
async performDownload(modelId, sessionId, signal, state) {
|
|
2730
|
+
const transformers = await this.getTransformersModule();
|
|
2731
|
+
await configureTransformersRuntime(transformers, this.cacheDir);
|
|
2732
|
+
transformers.env.remoteHost = buildTransformersRemoteHost(await this.readHuggingFaceEndpoint());
|
|
2733
|
+
const selectedGroup = selectLocalDownloadGroup(state.plan ?? null, state.plan?.selectedGroupId);
|
|
2734
|
+
const files = selectedGroup?.files ?? state.plan?.files ?? [];
|
|
2735
|
+
const totalBytes = selectedGroup?.estimatedTotalBytes ?? state.plan?.estimatedTotalBytes;
|
|
2736
|
+
const hfEndpoint = normalizeHuggingFaceEndpoint(await this.readHuggingFaceEndpoint());
|
|
2737
|
+
const downloadedFiles = buildDownloadStateFiles({
|
|
2738
|
+
planFiles: files,
|
|
2739
|
+
currentFiles: state.files
|
|
2740
|
+
});
|
|
2741
|
+
let bytesDownloaded = sumDownloadedBytes(downloadedFiles);
|
|
2742
|
+
if (files.length === 0) throw new Error("No concrete local model download files were selected.");
|
|
2743
|
+
for (const [fileIndex, file] of files.entries()) {
|
|
2744
|
+
throwIfAborted(signal);
|
|
2745
|
+
const previousFileBytes = downloadedFiles[fileIndex]?.downloadedBytes ?? 0;
|
|
2746
|
+
if (file.sizeBytes !== void 0 && previousFileBytes >= file.sizeBytes) continue;
|
|
2747
|
+
downloadedFiles[fileIndex] = {
|
|
2748
|
+
path: file.path,
|
|
2749
|
+
sizeBytes: file.sizeBytes,
|
|
2750
|
+
downloadedBytes: previousFileBytes
|
|
2751
|
+
};
|
|
2752
|
+
await this.emitDownloadProgress({
|
|
2753
|
+
modelId,
|
|
2754
|
+
sessionId,
|
|
2755
|
+
state,
|
|
2756
|
+
message: `Downloading ${file.path}.`,
|
|
2757
|
+
totalBytes,
|
|
2758
|
+
bytesDownloaded,
|
|
2759
|
+
files: downloadedFiles
|
|
2760
|
+
});
|
|
2761
|
+
const cachedPath = await downloadHuggingFaceFileToCacheDirWithProgress({
|
|
2762
|
+
repo: {
|
|
2763
|
+
type: "model",
|
|
2764
|
+
name: modelId
|
|
2765
|
+
},
|
|
2766
|
+
path: file.path,
|
|
2767
|
+
cacheDir: this.cacheDir,
|
|
2768
|
+
hubUrl: hfEndpoint,
|
|
2769
|
+
expectedSizeBytes: file.sizeBytes,
|
|
2770
|
+
retryPolicy: this.networkRetryPolicy,
|
|
2771
|
+
fetch: createAbortableFetch(signal),
|
|
2772
|
+
signal,
|
|
2773
|
+
onProgress: async (fileBytesDownloaded) => {
|
|
2774
|
+
throwIfAborted(signal);
|
|
2775
|
+
const boundedFileBytes = file.sizeBytes ? Math.min(file.sizeBytes, fileBytesDownloaded) : fileBytesDownloaded;
|
|
2776
|
+
downloadedFiles[fileIndex] = {
|
|
2777
|
+
path: file.path,
|
|
2778
|
+
sizeBytes: file.sizeBytes,
|
|
2779
|
+
downloadedBytes: boundedFileBytes
|
|
2780
|
+
};
|
|
2781
|
+
await this.emitDownloadProgress({
|
|
2782
|
+
modelId,
|
|
2783
|
+
sessionId,
|
|
2784
|
+
state,
|
|
2785
|
+
message: `Downloading ${file.path}.`,
|
|
2786
|
+
totalBytes,
|
|
2787
|
+
bytesDownloaded: bytesDownloaded - previousFileBytes + boundedFileBytes,
|
|
2788
|
+
files: downloadedFiles
|
|
2789
|
+
});
|
|
2790
|
+
},
|
|
2791
|
+
onRetry: async ({ retryDelayMs, phase }) => {
|
|
2792
|
+
const retryTarget = phase === "metadata" ? `metadata for ${file.path}` : `${file.path}`;
|
|
2793
|
+
await this.emitDownloadProgress({
|
|
2794
|
+
modelId,
|
|
2795
|
+
sessionId,
|
|
2796
|
+
state,
|
|
2797
|
+
message: `Connection interrupted while downloading ${retryTarget}. Retrying automatically in ${formatDuration(retryDelayMs)}.`,
|
|
2798
|
+
totalBytes,
|
|
2799
|
+
bytesDownloaded: bytesDownloaded - previousFileBytes + (downloadedFiles[fileIndex]?.downloadedBytes ?? 0),
|
|
2800
|
+
files: downloadedFiles
|
|
2801
|
+
});
|
|
2802
|
+
}
|
|
2803
|
+
});
|
|
2804
|
+
await mirrorHubCacheFileForTransformers({
|
|
2805
|
+
cacheDir: this.cacheDir,
|
|
2806
|
+
modelId,
|
|
2807
|
+
filePath: file.path,
|
|
2808
|
+
cachedPath
|
|
2809
|
+
});
|
|
2810
|
+
throwIfAborted(signal);
|
|
2811
|
+
const nextDownloadedBytes = file.sizeBytes ?? 0;
|
|
2812
|
+
bytesDownloaded = bytesDownloaded - previousFileBytes + nextDownloadedBytes;
|
|
2813
|
+
downloadedFiles[fileIndex] = {
|
|
2814
|
+
path: file.path,
|
|
2815
|
+
sizeBytes: file.sizeBytes,
|
|
2816
|
+
downloadedBytes: file.sizeBytes
|
|
2817
|
+
};
|
|
2818
|
+
await this.emitDownloadProgress({
|
|
2819
|
+
modelId,
|
|
2820
|
+
sessionId,
|
|
2821
|
+
state,
|
|
2822
|
+
message: `Downloaded ${file.path}.`,
|
|
2823
|
+
totalBytes,
|
|
2824
|
+
bytesDownloaded,
|
|
2825
|
+
files: downloadedFiles
|
|
2826
|
+
});
|
|
2827
|
+
}
|
|
2828
|
+
await this.finishDownload(modelId, sessionId, true, `Local model ${modelId} is ready.`);
|
|
2829
|
+
}
|
|
2830
|
+
async emitDownloadProgress(input) {
|
|
2831
|
+
if (!this.isActiveSession(input.modelId, input.sessionId)) return;
|
|
2832
|
+
const progress = input.totalBytes && input.totalBytes > 0 ? Math.max(0, Math.min(1, input.bytesDownloaded / input.totalBytes)) : void 0;
|
|
2833
|
+
const nextState = LocalModelAssetStateSchema.parse({
|
|
2834
|
+
...input.state,
|
|
2835
|
+
status: "downloading",
|
|
2836
|
+
progress,
|
|
2837
|
+
bytesDownloaded: input.bytesDownloaded,
|
|
2838
|
+
totalBytes: input.totalBytes,
|
|
2839
|
+
files: input.files,
|
|
2840
|
+
updatedAt: this.now(),
|
|
2841
|
+
resumable: true
|
|
2842
|
+
});
|
|
2843
|
+
await this.store.upsert(nextState);
|
|
2844
|
+
this.emitLog({
|
|
2845
|
+
engineId: "local",
|
|
2846
|
+
modelId: input.modelId,
|
|
2847
|
+
selectedGroupId: input.state.plan?.selectedGroupId,
|
|
2848
|
+
status: "downloading",
|
|
2849
|
+
message: input.message,
|
|
2850
|
+
progress,
|
|
2851
|
+
bytesDownloaded: input.bytesDownloaded,
|
|
2852
|
+
totalBytes: input.totalBytes,
|
|
2853
|
+
files: input.files,
|
|
2854
|
+
sessionId: input.sessionId,
|
|
2855
|
+
resumable: true,
|
|
2856
|
+
updatedAt: this.now()
|
|
2857
|
+
});
|
|
2858
|
+
}
|
|
2859
|
+
async finishDownload(modelId, sessionId, success, message) {
|
|
2860
|
+
if (!this.isActiveSession(modelId, sessionId)) return;
|
|
2861
|
+
const current = await this.readSelectedModelState(modelId);
|
|
2862
|
+
const nextState = LocalModelAssetStateSchema.parse({
|
|
2863
|
+
...current,
|
|
2864
|
+
status: success ? "downloaded" : "error",
|
|
2865
|
+
progress: success ? 1 : current.progress,
|
|
2866
|
+
bytesDownloaded: success ? current.totalBytes ?? current.bytesDownloaded : current.bytesDownloaded,
|
|
2867
|
+
totalBytes: current.totalBytes,
|
|
2868
|
+
installedAt: success ? this.now() : current.installedAt,
|
|
2869
|
+
updatedAt: this.now(),
|
|
2870
|
+
error: success ? void 0 : message,
|
|
2871
|
+
resumable: !success
|
|
2872
|
+
});
|
|
2873
|
+
await this.store.upsert(nextState);
|
|
2874
|
+
this.sessions.delete(modelId);
|
|
2875
|
+
this.emitLog({
|
|
2876
|
+
engineId: "local",
|
|
2877
|
+
modelId,
|
|
2878
|
+
selectedGroupId: nextState.plan?.selectedGroupId,
|
|
2879
|
+
status: nextState.status,
|
|
2880
|
+
message,
|
|
2881
|
+
progress: nextState.progress,
|
|
2882
|
+
bytesDownloaded: nextState.bytesDownloaded,
|
|
2883
|
+
totalBytes: nextState.totalBytes,
|
|
2884
|
+
sessionId,
|
|
2885
|
+
resumable: nextState.resumable,
|
|
2886
|
+
files: nextState.files,
|
|
2887
|
+
updatedAt: this.now()
|
|
2888
|
+
});
|
|
2889
|
+
}
|
|
2890
|
+
async readPlan(modelId, transformers, selectedGroupId) {
|
|
2891
|
+
return resolveLocalModelRuntimePlan({
|
|
2892
|
+
modelId,
|
|
2893
|
+
transformers,
|
|
2894
|
+
cacheDir: this.cacheDir,
|
|
2895
|
+
selectedGroupId: selectedGroupId ?? await this.readSelectedGroupId(),
|
|
2896
|
+
hfEndpoint: await this.readHuggingFaceEndpoint(),
|
|
2897
|
+
fetchCacheStore: this.fetchCacheStore
|
|
2898
|
+
}).catch(() => null);
|
|
2899
|
+
}
|
|
2900
|
+
async readPlanForState(modelId, selectedGroupId) {
|
|
2901
|
+
const transformers = await this.getTransformersModule();
|
|
2902
|
+
transformers.env.remoteHost = buildTransformersRemoteHost(await this.readHuggingFaceEndpoint());
|
|
2903
|
+
return this.readPlan(modelId, transformers, selectedGroupId);
|
|
2904
|
+
}
|
|
2905
|
+
async readSelectedModel() {
|
|
2906
|
+
return (await this.options.globalSettingsManager.readSettings()).translationEngines.local.model;
|
|
2907
|
+
}
|
|
2908
|
+
async readSelectedGroupId() {
|
|
2909
|
+
return (await this.options.globalSettingsManager.readSettings()).translationEngines.local.selectedGroupId;
|
|
2910
|
+
}
|
|
2911
|
+
async readHuggingFaceEndpoint() {
|
|
2912
|
+
return (await this.options.globalSettingsManager.readSettings()).translationEngines.local.hfEndpoint;
|
|
2913
|
+
}
|
|
2914
|
+
isActiveSession(modelId, sessionId) {
|
|
2915
|
+
return this.sessions.get(modelId)?.sessionId === sessionId;
|
|
2916
|
+
}
|
|
2917
|
+
emitLog(log) {
|
|
2918
|
+
this.logs.set(log.modelId, log);
|
|
2919
|
+
for (const listener of this.listeners) listener(log);
|
|
2920
|
+
}
|
|
2921
|
+
async getTransformersModule() {
|
|
2922
|
+
if (!this.transformersModulePromise) this.transformersModulePromise = this.loadTransformersModule();
|
|
2923
|
+
return this.transformersModulePromise;
|
|
2924
|
+
}
|
|
2925
|
+
async loadTransformersModule() {
|
|
2926
|
+
return import("@huggingface/transformers");
|
|
2927
|
+
}
|
|
2928
|
+
};
|
|
2929
|
+
function buildDownloadStateFiles(input) {
|
|
2930
|
+
const currentFileByPath = new Map(input.currentFiles.map((file) => [file.path, file]));
|
|
2931
|
+
return input.planFiles.map((file) => {
|
|
2932
|
+
const downloadedBytes = currentFileByPath.get(file.path)?.downloadedBytes;
|
|
2933
|
+
return {
|
|
2934
|
+
path: file.path,
|
|
2935
|
+
sizeBytes: file.sizeBytes,
|
|
2936
|
+
downloadedBytes: downloadedBytes === void 0 ? 0 : file.sizeBytes === void 0 ? downloadedBytes : Math.min(downloadedBytes, file.sizeBytes)
|
|
2937
|
+
};
|
|
2938
|
+
});
|
|
2939
|
+
}
|
|
2940
|
+
function sumDownloadedBytes(files) {
|
|
2941
|
+
return files.reduce((total, file) => {
|
|
2942
|
+
const downloadedBytes = file.downloadedBytes ?? 0;
|
|
2943
|
+
if (file.sizeBytes === void 0) return total + downloadedBytes;
|
|
2944
|
+
return total + Math.min(downloadedBytes, file.sizeBytes);
|
|
2945
|
+
}, 0);
|
|
2946
|
+
}
|
|
2947
|
+
function createAbortableFetch(signal) {
|
|
2948
|
+
return (input, init) => fetch(input, {
|
|
2949
|
+
...init,
|
|
2950
|
+
signal: mergeAbortSignals(init?.signal, signal)
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2953
|
+
function mergeAbortSignals(left, right) {
|
|
2954
|
+
if (!left) return right;
|
|
2955
|
+
if (left === right) return right;
|
|
2956
|
+
const controller = new AbortController();
|
|
2957
|
+
const abort = () => controller.abort();
|
|
2958
|
+
left.addEventListener("abort", abort, { once: true });
|
|
2959
|
+
right.addEventListener("abort", abort, { once: true });
|
|
2960
|
+
if (left.aborted || right.aborted) controller.abort();
|
|
2961
|
+
return controller.signal;
|
|
2962
|
+
}
|
|
2963
|
+
function throwIfAborted(signal) {
|
|
2964
|
+
if (signal.aborted) throw new Error("Local model download aborted.");
|
|
2965
|
+
}
|
|
2966
|
+
async function downloadHuggingFaceFileToCacheDirWithProgress(input) {
|
|
2967
|
+
const revision = "main";
|
|
2968
|
+
let lastError;
|
|
2969
|
+
const info = await readHuggingFaceFileDownloadInfoWithRetry({
|
|
2970
|
+
repo: input.repo,
|
|
2971
|
+
path: input.path,
|
|
2972
|
+
revision,
|
|
2973
|
+
hubUrl: input.hubUrl,
|
|
2974
|
+
retryPolicy: input.retryPolicy,
|
|
2975
|
+
fetch: input.fetch,
|
|
2976
|
+
signal: input.signal,
|
|
2977
|
+
onRetry: input.onRetry
|
|
2978
|
+
});
|
|
2979
|
+
if (!info) throw new Error(`Cannot get path info for ${input.path}.`);
|
|
2980
|
+
const totalBytes = input.expectedSizeBytes ?? info.size;
|
|
2981
|
+
if (totalBytes === void 0) throw new Error(`Cannot get path info for ${input.path}.`);
|
|
2982
|
+
const cachePaths = getHubCacheFilePaths({
|
|
2983
|
+
cacheDir: input.cacheDir,
|
|
2984
|
+
modelId: input.repo.name,
|
|
2985
|
+
filePath: input.path,
|
|
2986
|
+
revision,
|
|
2987
|
+
etag: info.etag
|
|
2988
|
+
});
|
|
2989
|
+
const existingPointerSize = await readPathSize(cachePaths.pointerPath);
|
|
2990
|
+
if (existingPointerSize !== null && existingPointerSize >= totalBytes) {
|
|
2991
|
+
await input.onProgress(totalBytes);
|
|
2992
|
+
return cachePaths.pointerPath;
|
|
2993
|
+
}
|
|
2994
|
+
for (let attempt = 0; attempt <= input.retryPolicy.limit; attempt += 1) try {
|
|
2995
|
+
throwIfAborted(input.signal);
|
|
2996
|
+
let resumeBytes = await readPathSize(cachePaths.incompletePath);
|
|
2997
|
+
if (resumeBytes !== null && resumeBytes > totalBytes) {
|
|
2998
|
+
await rm(cachePaths.incompletePath, { force: true });
|
|
2999
|
+
resumeBytes = 0;
|
|
3000
|
+
}
|
|
3001
|
+
if (resumeBytes !== null && resumeBytes > 0) await input.onProgress(Math.min(resumeBytes, totalBytes));
|
|
3002
|
+
if (!await streamDownloadToIncompleteFile({
|
|
3003
|
+
url: info.url,
|
|
3004
|
+
incompletePath: cachePaths.incompletePath,
|
|
3005
|
+
startBytes: resumeBytes ?? 0,
|
|
3006
|
+
totalBytes,
|
|
3007
|
+
accessToken: void 0,
|
|
3008
|
+
fetch: input.fetch,
|
|
3009
|
+
signal: input.signal,
|
|
3010
|
+
onProgress: input.onProgress
|
|
3011
|
+
})) {
|
|
3012
|
+
const blob = await downloadFile({
|
|
3013
|
+
repo: input.repo,
|
|
3014
|
+
path: input.path,
|
|
3015
|
+
revision,
|
|
3016
|
+
hubUrl: input.hubUrl,
|
|
3017
|
+
fetch: input.fetch,
|
|
3018
|
+
downloadInfo: info,
|
|
3019
|
+
xet: false
|
|
3020
|
+
});
|
|
3021
|
+
if (!blob) throw new Error(`Invalid response for file ${input.path}.`);
|
|
3022
|
+
await appendBlobToIncompleteFile({
|
|
3023
|
+
blob: resumeBytes && resumeBytes > 0 ? blob.slice(resumeBytes, totalBytes) : blob,
|
|
3024
|
+
incompletePath: cachePaths.incompletePath,
|
|
3025
|
+
startBytes: resumeBytes ?? 0,
|
|
3026
|
+
totalBytes,
|
|
3027
|
+
onProgress: input.onProgress
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
await finalizeHubCacheFile(cachePaths);
|
|
3031
|
+
await input.onProgress(totalBytes);
|
|
3032
|
+
return cachePaths.pointerPath;
|
|
3033
|
+
} catch (error) {
|
|
3034
|
+
lastError = error;
|
|
3035
|
+
if (!isRetryableDownloadError(error) || attempt === input.retryPolicy.limit) throw error;
|
|
3036
|
+
const retryDelayMs = Math.min(input.retryPolicy.maxDelayMs, input.retryPolicy.delayMs * (attempt + 1));
|
|
3037
|
+
await input.onRetry?.({
|
|
3038
|
+
retryDelayMs,
|
|
3039
|
+
phase: "download"
|
|
3040
|
+
});
|
|
3041
|
+
await delay(retryDelayMs, input.signal);
|
|
3042
|
+
}
|
|
3043
|
+
throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error(`Cannot download ${input.path}.`);
|
|
3044
|
+
}
|
|
3045
|
+
async function readHuggingFaceFileDownloadInfoWithRetry(input) {
|
|
3046
|
+
let lastError;
|
|
3047
|
+
for (let attempt = 0; attempt <= input.retryPolicy.limit; attempt += 1) try {
|
|
3048
|
+
throwIfAborted(input.signal);
|
|
3049
|
+
return await fileDownloadInfo({
|
|
3050
|
+
repo: input.repo,
|
|
3051
|
+
path: input.path,
|
|
3052
|
+
revision: input.revision,
|
|
3053
|
+
hubUrl: input.hubUrl,
|
|
3054
|
+
fetch: input.fetch
|
|
3055
|
+
});
|
|
3056
|
+
} catch (error) {
|
|
3057
|
+
lastError = error;
|
|
3058
|
+
if (!isRetryableDownloadError(error) || attempt === input.retryPolicy.limit) throw error;
|
|
3059
|
+
const retryDelayMs = Math.min(input.retryPolicy.maxDelayMs, input.retryPolicy.delayMs * (attempt + 1));
|
|
3060
|
+
await input.onRetry?.({
|
|
3061
|
+
retryDelayMs,
|
|
3062
|
+
phase: "metadata"
|
|
3063
|
+
});
|
|
3064
|
+
await delay(retryDelayMs, input.signal);
|
|
3065
|
+
}
|
|
3066
|
+
throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error(`Cannot get path info for ${input.path}.`);
|
|
3067
|
+
}
|
|
3068
|
+
async function appendBlobToIncompleteFile(input) {
|
|
3069
|
+
await mkdir(dirname(input.incompletePath), { recursive: true });
|
|
3070
|
+
const fileHandle = await open(input.incompletePath, "a");
|
|
3071
|
+
const reader = input.blob.stream().getReader();
|
|
3072
|
+
let downloadedBytes = input.startBytes;
|
|
3073
|
+
try {
|
|
3074
|
+
while (true) {
|
|
3075
|
+
const result = await reader.read();
|
|
3076
|
+
if (result.done) break;
|
|
3077
|
+
await fileHandle.write(result.value);
|
|
3078
|
+
downloadedBytes += result.value.byteLength;
|
|
3079
|
+
await input.onProgress(Math.min(downloadedBytes, input.totalBytes));
|
|
3080
|
+
}
|
|
3081
|
+
} finally {
|
|
3082
|
+
await reader.cancel().catch(() => void 0);
|
|
3083
|
+
await fileHandle.close();
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
async function streamDownloadToIncompleteFile(input) {
|
|
3087
|
+
const headers = new Headers();
|
|
3088
|
+
if (input.accessToken) headers.set("Authorization", `Bearer ${input.accessToken}`);
|
|
3089
|
+
if (input.startBytes > 0) headers.set("Range", `bytes=${input.startBytes}-`);
|
|
3090
|
+
const response = await input.fetch(input.url, {
|
|
3091
|
+
method: "GET",
|
|
3092
|
+
headers,
|
|
3093
|
+
signal: input.signal
|
|
3094
|
+
});
|
|
3095
|
+
if (!response.ok && response.status !== 206) throw new Error(`Invalid response for file download: status ${response.status}.`);
|
|
3096
|
+
if (!response.body) return false;
|
|
3097
|
+
await mkdir(dirname(input.incompletePath), { recursive: true });
|
|
3098
|
+
const fileHandle = await open(input.incompletePath, input.startBytes > 0 ? "a" : "w");
|
|
3099
|
+
const reader = response.body.getReader();
|
|
3100
|
+
let downloadedBytes = input.startBytes;
|
|
3101
|
+
try {
|
|
3102
|
+
while (true) {
|
|
3103
|
+
const result = await reader.read();
|
|
3104
|
+
if (result.done) break;
|
|
3105
|
+
await fileHandle.write(result.value);
|
|
3106
|
+
downloadedBytes += result.value.byteLength;
|
|
3107
|
+
await input.onProgress(Math.min(downloadedBytes, input.totalBytes));
|
|
3108
|
+
}
|
|
3109
|
+
} finally {
|
|
3110
|
+
await reader.cancel().catch(() => void 0);
|
|
3111
|
+
await fileHandle.close();
|
|
3112
|
+
}
|
|
3113
|
+
return true;
|
|
3114
|
+
}
|
|
3115
|
+
async function finalizeHubCacheFile(input) {
|
|
3116
|
+
await mkdir(dirname(input.blobPath), { recursive: true });
|
|
3117
|
+
await mkdir(dirname(input.pointerPath), { recursive: true });
|
|
3118
|
+
await rm(input.blobPath, { force: true });
|
|
3119
|
+
await rename(input.incompletePath, input.blobPath);
|
|
3120
|
+
await unlink(input.pointerPath).catch(() => void 0);
|
|
3121
|
+
await symlink(input.blobPath, input.pointerPath);
|
|
3122
|
+
}
|
|
3123
|
+
function getHubCacheFilePaths(input) {
|
|
3124
|
+
const repoPath = getHubCacheRepoPath(input.cacheDir, input.modelId);
|
|
3125
|
+
const snapshotId = sanitizeEtag(input.etag) || sanitizeId(input.revision);
|
|
3126
|
+
const blobPath = join(repoPath, "blobs", sanitizeEtag(input.etag));
|
|
3127
|
+
return {
|
|
3128
|
+
blobPath,
|
|
3129
|
+
incompletePath: `${blobPath}.incomplete`,
|
|
3130
|
+
pointerPath: join(repoPath, "snapshots", snapshotId, input.filePath)
|
|
3131
|
+
};
|
|
3132
|
+
}
|
|
3133
|
+
async function readPathSize(path) {
|
|
3134
|
+
try {
|
|
3135
|
+
return (await stat(path)).size;
|
|
3136
|
+
} catch {
|
|
3137
|
+
return null;
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
function isRetryableDownloadError(error) {
|
|
3141
|
+
return isRetryableNetworkError(error, { treatUnknownAsRetryable: true });
|
|
3142
|
+
}
|
|
3143
|
+
function delay(ms, signal) {
|
|
3144
|
+
return new Promise((resolve$1, reject) => {
|
|
3145
|
+
const timer = setTimeout(() => {
|
|
3146
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3147
|
+
resolve$1();
|
|
3148
|
+
}, ms);
|
|
3149
|
+
const onAbort = () => {
|
|
3150
|
+
clearTimeout(timer);
|
|
3151
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3152
|
+
reject(/* @__PURE__ */ new Error("Local model download aborted."));
|
|
3153
|
+
};
|
|
3154
|
+
if (signal) {
|
|
3155
|
+
if (signal.aborted) {
|
|
3156
|
+
clearTimeout(timer);
|
|
3157
|
+
reject(/* @__PURE__ */ new Error("Local model download aborted."));
|
|
3158
|
+
return;
|
|
3159
|
+
}
|
|
3160
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3161
|
+
}
|
|
3162
|
+
});
|
|
3163
|
+
}
|
|
3164
|
+
function formatDuration(ms) {
|
|
3165
|
+
if (ms < 1e3) return `${ms} ms`;
|
|
3166
|
+
const seconds = ms / 1e3;
|
|
3167
|
+
return seconds >= 10 ? `${Math.round(seconds)} s` : `${seconds.toFixed(1)} s`;
|
|
3168
|
+
}
|
|
3169
|
+
async function mirrorHubCacheFileForTransformers(input) {
|
|
3170
|
+
const sourcePath = await resolveRealCacheFile(input.cachedPath);
|
|
3171
|
+
await copyFileIfMissing(sourcePath, join(getTransformersLocalModelPath(input.cacheDir, input.modelId), input.filePath));
|
|
3172
|
+
await copyFileIfMissing(sourcePath, join(getTransformersFileCacheModelPath(input.cacheDir, input.modelId), input.filePath));
|
|
3173
|
+
}
|
|
3174
|
+
async function copyFileIfMissing(sourcePath, targetPath) {
|
|
3175
|
+
if (existsSync(targetPath)) return;
|
|
3176
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
3177
|
+
await copyFile(sourcePath, targetPath);
|
|
3178
|
+
}
|
|
3179
|
+
async function readSymlinkTarget(path) {
|
|
3180
|
+
return readlink(path);
|
|
3181
|
+
}
|
|
3182
|
+
async function resolveRealCacheFile(path) {
|
|
3183
|
+
if (!(await lstat(path)).isSymbolicLink()) return path;
|
|
3184
|
+
return resolve(dirname(path), await readSymlinkTarget(path));
|
|
3185
|
+
}
|
|
3186
|
+
function getHubCacheRepoPath(cacheDir, modelId) {
|
|
3187
|
+
return join(cacheDir, `models--${modelId.split("/").join("--")}`);
|
|
3188
|
+
}
|
|
3189
|
+
function toCatalogItem(candidate, asset) {
|
|
3190
|
+
const hasSelectableGroup = candidate.downloadGroups?.some((group) => group.selectable) ?? false;
|
|
3191
|
+
return {
|
|
3192
|
+
...candidate,
|
|
3193
|
+
asset,
|
|
3194
|
+
selectable: hasSelectableGroup || (candidate.size.estimatedTotalBytes ?? 0) > 0,
|
|
3195
|
+
local: asset.status === "downloaded" || asset.status === "paused" || asset.status === "downloading" || (asset.progress ?? 0) > 0
|
|
3196
|
+
};
|
|
3197
|
+
}
|
|
3198
|
+
function compareCatalogItems(left, right) {
|
|
3199
|
+
if (left.local !== right.local) return left.local ? -1 : 1;
|
|
3200
|
+
if (left.asset.selected !== right.asset.selected) return left.asset.selected ? -1 : 1;
|
|
3201
|
+
const rightProgress = right.asset.progress ?? 0;
|
|
3202
|
+
const leftProgress = left.asset.progress ?? 0;
|
|
3203
|
+
if (left.local && right.local && leftProgress !== rightProgress) return rightProgress - leftProgress;
|
|
3204
|
+
return right.downloads - left.downloads;
|
|
3205
|
+
}
|
|
3206
|
+
function sanitizeId(value) {
|
|
3207
|
+
return value.replace(/[^a-zA-Z0-9_-]+/g, "-");
|
|
3208
|
+
}
|
|
3209
|
+
function sanitizeEtag(value) {
|
|
3210
|
+
return sanitizeId(value.replace(/^W\//, "").replace(/^"+|"+$/g, ""));
|
|
3211
|
+
}
|
|
3212
|
+
function formatBytes(value) {
|
|
3213
|
+
if (!Number.isFinite(value) || value <= 0) return "0 B";
|
|
3214
|
+
const units = [
|
|
3215
|
+
"B",
|
|
3216
|
+
"KB",
|
|
3217
|
+
"MB",
|
|
3218
|
+
"GB"
|
|
3219
|
+
];
|
|
3220
|
+
let size = value;
|
|
3221
|
+
let unitIndex = 0;
|
|
3222
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
3223
|
+
size /= 1024;
|
|
3224
|
+
unitIndex += 1;
|
|
3225
|
+
}
|
|
3226
|
+
const digits = size >= 100 || unitIndex === 0 ? 0 : 1;
|
|
3227
|
+
return `${size.toFixed(digits)} ${units[unitIndex]}`;
|
|
3228
|
+
}
|
|
3229
|
+
|
|
1451
3230
|
//#endregion
|
|
1452
3231
|
//#region src/notification-service.ts
|
|
1453
3232
|
var NotificationService = class {
|
|
@@ -3085,7 +4864,13 @@ const globalSettingsRouter = router({
|
|
|
3085
4864
|
get: publicProcedure.query(({ ctx }) => {
|
|
3086
4865
|
return ctx.globalSettingsManager.readSettings();
|
|
3087
4866
|
}),
|
|
3088
|
-
update: publicProcedure.input(OpenSpecUIGlobalSettingsSchema.partial().extend({
|
|
4867
|
+
update: publicProcedure.input(OpenSpecUIGlobalSettingsSchema.partial().extend({
|
|
4868
|
+
translationCache: TranslationCacheSettingsSchema.partial().optional(),
|
|
4869
|
+
translationEngines: z.object({
|
|
4870
|
+
openai: TranslationOpenAISettingsSchema.partial().optional(),
|
|
4871
|
+
local: TranslationLocalSettingsSchema.partial().optional()
|
|
4872
|
+
}).optional()
|
|
4873
|
+
})).mutation(async ({ ctx, input }) => {
|
|
3089
4874
|
await ctx.globalSettingsManager.writeSettings(input);
|
|
3090
4875
|
return { success: true };
|
|
3091
4876
|
}),
|
|
@@ -3110,6 +4895,96 @@ const translationCacheRouter = router({
|
|
|
3110
4895
|
return ctx.translationCacheService.clear();
|
|
3111
4896
|
})
|
|
3112
4897
|
});
|
|
4898
|
+
const translationEnginesRouter = router({
|
|
4899
|
+
list: publicProcedure.query(({ ctx }) => {
|
|
4900
|
+
return ctx.translationEngineService.listEngines();
|
|
4901
|
+
}),
|
|
4902
|
+
searchModels: publicProcedure.input(z.object({
|
|
4903
|
+
engineId: ServiceTranslationEngineIdSchema,
|
|
4904
|
+
query: z.string().optional(),
|
|
4905
|
+
sourceLanguage: z.string().optional(),
|
|
4906
|
+
targetLanguage: z.string().optional(),
|
|
4907
|
+
limit: z.number().int().positive().max(20).optional(),
|
|
4908
|
+
cursor: z.string().optional()
|
|
4909
|
+
})).query(({ ctx, input }) => {
|
|
4910
|
+
return ctx.translationEngineService.searchModels(input);
|
|
4911
|
+
}),
|
|
4912
|
+
getModelDownloadPlan: publicProcedure.input(z.object({
|
|
4913
|
+
engineId: ServiceTranslationEngineIdSchema,
|
|
4914
|
+
model: z.string().min(1),
|
|
4915
|
+
selectedGroupId: z.string().min(1).optional()
|
|
4916
|
+
})).query(({ ctx, input }) => {
|
|
4917
|
+
return ctx.translationEngineService.getModelDownloadPlan(input);
|
|
4918
|
+
}),
|
|
4919
|
+
select: publicProcedure.input(z.object({ engineId: TranslationEngineIdSchema })).mutation(({ ctx, input }) => {
|
|
4920
|
+
return ctx.translationEngineService.selectEngine(input.engineId);
|
|
4921
|
+
}),
|
|
4922
|
+
batchTranslate: publicProcedure.input(BatchTranslateInputSchema).subscription(({ ctx, input }) => {
|
|
4923
|
+
return ctx.translationEngineService.batchTranslate(input);
|
|
4924
|
+
})
|
|
4925
|
+
});
|
|
4926
|
+
const localModelsRouter = router({
|
|
4927
|
+
listLocal: publicProcedure.query(({ ctx }) => {
|
|
4928
|
+
return ctx.localModelAssetService.listLocalCatalog();
|
|
4929
|
+
}),
|
|
4930
|
+
searchRemote: publicProcedure.input(z.object({
|
|
4931
|
+
requestId: z.string().min(1).optional(),
|
|
4932
|
+
query: z.string().optional(),
|
|
4933
|
+
sourceLanguage: z.string().optional(),
|
|
4934
|
+
targetLanguage: z.string().optional(),
|
|
4935
|
+
limit: z.number().int().positive().max(20).optional(),
|
|
4936
|
+
cursor: z.string().optional()
|
|
4937
|
+
})).query(({ ctx, input }) => {
|
|
4938
|
+
return ctx.localModelAssetService.searchRemoteCatalog({
|
|
4939
|
+
engineId: "local",
|
|
4940
|
+
...input
|
|
4941
|
+
});
|
|
4942
|
+
}),
|
|
4943
|
+
searchRemoteStream: publicProcedure.input(z.object({
|
|
4944
|
+
requestId: z.string().min(1),
|
|
4945
|
+
query: z.string().optional(),
|
|
4946
|
+
sourceLanguage: z.string().optional(),
|
|
4947
|
+
targetLanguage: z.string().optional(),
|
|
4948
|
+
limit: z.number().int().positive().max(20).optional(),
|
|
4949
|
+
cursor: z.string().optional()
|
|
4950
|
+
})).subscription(({ ctx, input }) => {
|
|
4951
|
+
return ctx.localModelAssetService.subscribeRemoteCatalog({
|
|
4952
|
+
engineId: "local",
|
|
4953
|
+
...input
|
|
4954
|
+
});
|
|
4955
|
+
}),
|
|
4956
|
+
state: publicProcedure.input(z.object({
|
|
4957
|
+
modelId: z.string().min(1),
|
|
4958
|
+
selectedGroupId: z.string().min(1).optional()
|
|
4959
|
+
})).query(({ ctx, input }) => {
|
|
4960
|
+
return ctx.localModelAssetService.readSelectedModelState(input.modelId, input.selectedGroupId);
|
|
4961
|
+
}),
|
|
4962
|
+
subscribeLogs: publicProcedure.subscription(({ ctx }) => {
|
|
4963
|
+
return ctx.localModelAssetService.subscribeLogs();
|
|
4964
|
+
}),
|
|
4965
|
+
markSelected: publicProcedure.input(z.object({ modelId: z.string().min(1) })).mutation(async ({ ctx, input }) => {
|
|
4966
|
+
await ctx.localModelAssetService.markSelectedModel(input.modelId);
|
|
4967
|
+
return { success: true };
|
|
4968
|
+
}),
|
|
4969
|
+
download: publicProcedure.input(z.object({
|
|
4970
|
+
modelId: z.string().min(1),
|
|
4971
|
+
selectedGroupId: z.string().min(1).optional()
|
|
4972
|
+
})).mutation(async ({ ctx, input }) => {
|
|
4973
|
+
return ctx.localModelAssetService.startDownload(input.modelId, input.selectedGroupId);
|
|
4974
|
+
}),
|
|
4975
|
+
pause: publicProcedure.input(z.object({ modelId: z.string().min(1) })).mutation(({ ctx, input }) => {
|
|
4976
|
+
return ctx.localModelAssetService.pauseDownload(input.modelId);
|
|
4977
|
+
}),
|
|
4978
|
+
resume: publicProcedure.input(z.object({
|
|
4979
|
+
modelId: z.string().min(1),
|
|
4980
|
+
selectedGroupId: z.string().min(1).optional()
|
|
4981
|
+
})).mutation(async ({ ctx, input }) => {
|
|
4982
|
+
return ctx.localModelAssetService.resumeDownload(input.modelId, input.selectedGroupId);
|
|
4983
|
+
}),
|
|
4984
|
+
delete: publicProcedure.input(z.object({ modelId: z.string().min(1) })).mutation(({ ctx, input }) => {
|
|
4985
|
+
return ctx.localModelAssetService.deleteModel(input.modelId);
|
|
4986
|
+
})
|
|
4987
|
+
});
|
|
3113
4988
|
const OPSX_CORE_PROFILE_WORKFLOWS = [
|
|
3114
4989
|
"propose",
|
|
3115
4990
|
"explore",
|
|
@@ -3552,7 +5427,13 @@ const configRouter = router({
|
|
|
3552
5427
|
dashboard: DashboardConfigSchema.partial().optional(),
|
|
3553
5428
|
git: GitConfigSchema.partial().optional(),
|
|
3554
5429
|
notifications: NotificationSettingsSchema.partial().optional(),
|
|
3555
|
-
translation: DocumentTranslationConfigSchema.partial().
|
|
5430
|
+
translation: DocumentTranslationConfigSchema.partial().extend({ engines: z.object({
|
|
5431
|
+
local: z.object({
|
|
5432
|
+
model: z.string().min(1).optional(),
|
|
5433
|
+
selectedGroupId: z.string().min(1).optional()
|
|
5434
|
+
}).optional(),
|
|
5435
|
+
openai: z.object({ model: z.string().min(1).optional() }).optional()
|
|
5436
|
+
}).optional() }).optional()
|
|
3556
5437
|
})).mutation(async ({ ctx, input }) => {
|
|
3557
5438
|
const hasCliCommand = input.cli !== void 0 && Object.prototype.hasOwnProperty.call(input.cli, "command");
|
|
3558
5439
|
const hasCliArgs = input.cli !== void 0 && Object.prototype.hasOwnProperty.call(input.cli, "args");
|
|
@@ -4207,6 +6088,8 @@ const appRouter = router({
|
|
|
4207
6088
|
config: configRouter,
|
|
4208
6089
|
globalSettings: globalSettingsRouter,
|
|
4209
6090
|
translationCache: translationCacheRouter,
|
|
6091
|
+
translationEngines: translationEnginesRouter,
|
|
6092
|
+
localModels: localModelsRouter,
|
|
4210
6093
|
notifications: notificationsRouter,
|
|
4211
6094
|
sounds: soundsRouter,
|
|
4212
6095
|
cli: cliRouter,
|
|
@@ -4371,6 +6254,10 @@ var SqliteTranslationCacheAdapter = class {
|
|
|
4371
6254
|
placeholder_topology_hash TEXT NOT NULL,
|
|
4372
6255
|
attribute_topology_hash TEXT NOT NULL,
|
|
4373
6256
|
display_policy_version INTEGER NOT NULL,
|
|
6257
|
+
engine_id TEXT NOT NULL DEFAULT 'browser',
|
|
6258
|
+
engine_version TEXT,
|
|
6259
|
+
model TEXT,
|
|
6260
|
+
translator_contract_version INTEGER NOT NULL DEFAULT 1,
|
|
4374
6261
|
created_at INTEGER NOT NULL,
|
|
4375
6262
|
last_accessed_at INTEGER NOT NULL
|
|
4376
6263
|
);
|
|
@@ -4378,13 +6265,18 @@ var SqliteTranslationCacheAdapter = class {
|
|
|
4378
6265
|
ON translation_cache_entries(last_accessed_at ASC);
|
|
4379
6266
|
`);
|
|
4380
6267
|
ensureTargetNodesJsonColumn(database);
|
|
6268
|
+
ensureColumn(database, "engine_id", "TEXT NOT NULL DEFAULT 'browser'");
|
|
6269
|
+
ensureColumn(database, "engine_version", "TEXT");
|
|
6270
|
+
ensureColumn(database, "model", "TEXT");
|
|
6271
|
+
ensureColumn(database, "translator_contract_version", "INTEGER NOT NULL DEFAULT 1");
|
|
4381
6272
|
this.database = database;
|
|
4382
6273
|
}
|
|
4383
6274
|
async read(keyHash, now) {
|
|
4384
6275
|
const database = await this.requireDatabase();
|
|
4385
6276
|
const row = database.prepare(`SELECT key_hash, cache_key, source_text, translated_text, target_nodes_json, source_language,
|
|
4386
6277
|
target_language, placeholder_topology_hash, attribute_topology_hash,
|
|
4387
|
-
display_policy_version,
|
|
6278
|
+
display_policy_version, engine_id, engine_version, model, translator_contract_version,
|
|
6279
|
+
created_at, last_accessed_at
|
|
4388
6280
|
FROM translation_cache_entries
|
|
4389
6281
|
WHERE key_hash = ?`).get(keyHash);
|
|
4390
6282
|
if (!isSqliteTranslationCacheRow(row)) return null;
|
|
@@ -4400,16 +6292,23 @@ var SqliteTranslationCacheAdapter = class {
|
|
|
4400
6292
|
placeholderTopologyHash: row.placeholder_topology_hash,
|
|
4401
6293
|
attributeTopologyHash: row.attribute_topology_hash,
|
|
4402
6294
|
displayPolicyVersion: row.display_policy_version,
|
|
6295
|
+
engineId: row.engine_id,
|
|
6296
|
+
...row.engine_version ? { engineVersion: row.engine_version } : {},
|
|
6297
|
+
...row.model ? { model: row.model } : {},
|
|
6298
|
+
translatorContractVersion: row.translator_contract_version,
|
|
4403
6299
|
createdAt: row.created_at,
|
|
4404
6300
|
lastAccessedAt: now
|
|
4405
6301
|
};
|
|
4406
6302
|
}
|
|
4407
6303
|
async write(input, now) {
|
|
4408
|
-
|
|
6304
|
+
const database = await this.requireDatabase();
|
|
6305
|
+
const entry = TranslationCacheWriteInputSchema.parse(input);
|
|
6306
|
+
database.prepare(`INSERT INTO translation_cache_entries (
|
|
4409
6307
|
key_hash, cache_key, source_text, translated_text, target_nodes_json, source_language,
|
|
4410
6308
|
target_language, placeholder_topology_hash, attribute_topology_hash,
|
|
4411
|
-
display_policy_version,
|
|
4412
|
-
|
|
6309
|
+
display_policy_version, engine_id, engine_version, model, translator_contract_version,
|
|
6310
|
+
created_at, last_accessed_at
|
|
6311
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4413
6312
|
ON CONFLICT(key_hash) DO UPDATE SET
|
|
4414
6313
|
cache_key = excluded.cache_key,
|
|
4415
6314
|
source_text = excluded.source_text,
|
|
@@ -4420,7 +6319,11 @@ var SqliteTranslationCacheAdapter = class {
|
|
|
4420
6319
|
placeholder_topology_hash = excluded.placeholder_topology_hash,
|
|
4421
6320
|
attribute_topology_hash = excluded.attribute_topology_hash,
|
|
4422
6321
|
display_policy_version = excluded.display_policy_version,
|
|
4423
|
-
|
|
6322
|
+
engine_id = excluded.engine_id,
|
|
6323
|
+
engine_version = excluded.engine_version,
|
|
6324
|
+
model = excluded.model,
|
|
6325
|
+
translator_contract_version = excluded.translator_contract_version,
|
|
6326
|
+
last_accessed_at = excluded.last_accessed_at`).run(entry.keyHash, entry.key, entry.sourceText, entry.translatedText, entry.targetNodesJson ?? null, entry.sourceLanguage, entry.targetLanguage, entry.placeholderTopologyHash, entry.attributeTopologyHash, entry.displayPolicyVersion, entry.engineId, entry.engineVersion ?? null, entry.model ?? null, entry.translatorContractVersion, now, now);
|
|
4424
6327
|
}
|
|
4425
6328
|
async count() {
|
|
4426
6329
|
return readSqliteCount((await this.requireDatabase()).prepare("SELECT COUNT(*) AS count FROM translation_cache_entries").get());
|
|
@@ -4480,15 +6383,18 @@ function isBunRuntime() {
|
|
|
4480
6383
|
return typeof process.versions.bun === "string";
|
|
4481
6384
|
}
|
|
4482
6385
|
function ensureTargetNodesJsonColumn(database) {
|
|
6386
|
+
ensureColumn(database, "target_nodes_json", "TEXT");
|
|
6387
|
+
}
|
|
6388
|
+
function ensureColumn(database, columnName, definition) {
|
|
4483
6389
|
if (!database.prepare("PRAGMA table_info(translation_cache_entries)").all().some((row) => {
|
|
4484
6390
|
if (!row || typeof row !== "object") return false;
|
|
4485
|
-
return row.name ===
|
|
4486
|
-
})) database.exec(
|
|
6391
|
+
return row.name === columnName;
|
|
6392
|
+
})) database.exec(`ALTER TABLE translation_cache_entries ADD COLUMN ${columnName} ${definition}`);
|
|
4487
6393
|
}
|
|
4488
6394
|
function isSqliteTranslationCacheRow(value) {
|
|
4489
6395
|
if (!value || typeof value !== "object") return false;
|
|
4490
6396
|
const row = value;
|
|
4491
|
-
return typeof row.key_hash === "string" && typeof row.cache_key === "string" && typeof row.source_text === "string" && typeof row.translated_text === "string" && (typeof row.target_nodes_json === "string" || row.target_nodes_json === null) && typeof row.source_language === "string" && typeof row.target_language === "string" && typeof row.placeholder_topology_hash === "string" && typeof row.attribute_topology_hash === "string" && typeof row.display_policy_version === "number" && typeof row.created_at === "number" && typeof row.last_accessed_at === "number";
|
|
6397
|
+
return typeof row.key_hash === "string" && typeof row.cache_key === "string" && typeof row.source_text === "string" && typeof row.translated_text === "string" && (typeof row.target_nodes_json === "string" || row.target_nodes_json === null) && typeof row.source_language === "string" && typeof row.target_language === "string" && typeof row.placeholder_topology_hash === "string" && typeof row.attribute_topology_hash === "string" && typeof row.display_policy_version === "number" && (row.engine_id === "browser" || row.engine_id === "local" || row.engine_id === "openai") && (typeof row.engine_version === "string" || row.engine_version === null) && (typeof row.model === "string" || row.model === null) && typeof row.translator_contract_version === "number" && typeof row.created_at === "number" && typeof row.last_accessed_at === "number";
|
|
4492
6398
|
}
|
|
4493
6399
|
function readSqliteCount(value) {
|
|
4494
6400
|
if (!value || typeof value !== "object") return 0;
|
|
@@ -4496,18 +6402,6 @@ function readSqliteCount(value) {
|
|
|
4496
6402
|
return typeof count === "number" ? count : 0;
|
|
4497
6403
|
}
|
|
4498
6404
|
|
|
4499
|
-
//#endregion
|
|
4500
|
-
//#region src/translation-cache-path.ts
|
|
4501
|
-
function getDefaultTranslationCacheDatabasePath() {
|
|
4502
|
-
return join(getOpenSpecUICacheDir(), "translation-cache.sqlite");
|
|
4503
|
-
}
|
|
4504
|
-
function getOpenSpecUICacheDir() {
|
|
4505
|
-
const currentPlatform = platform();
|
|
4506
|
-
if (currentPlatform === "darwin") return join(homedir(), "Library", "Caches", "openspecui");
|
|
4507
|
-
if (currentPlatform === "win32") return join(process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local"), "OpenSpecUI", "Cache");
|
|
4508
|
-
return join(process.env.XDG_CACHE_HOME || join(homedir(), ".cache"), "openspecui");
|
|
4509
|
-
}
|
|
4510
|
-
|
|
4511
6405
|
//#endregion
|
|
4512
6406
|
//#region src/translation-cache-service.ts
|
|
4513
6407
|
var TranslationCacheService = class {
|
|
@@ -4568,6 +6462,225 @@ var TranslationCacheService = class {
|
|
|
4568
6462
|
}
|
|
4569
6463
|
};
|
|
4570
6464
|
|
|
6465
|
+
//#endregion
|
|
6466
|
+
//#region src/translation-engine-service.ts
|
|
6467
|
+
var TranslationEngineService = class {
|
|
6468
|
+
projectDir;
|
|
6469
|
+
configManager;
|
|
6470
|
+
globalSettingsManager;
|
|
6471
|
+
now;
|
|
6472
|
+
localCacheDir;
|
|
6473
|
+
localAssetStore;
|
|
6474
|
+
localFetchCacheStore;
|
|
6475
|
+
constructor(options) {
|
|
6476
|
+
ensureProxyAwareFetchDispatcher();
|
|
6477
|
+
this.projectDir = options.projectDir;
|
|
6478
|
+
this.configManager = options.configManager;
|
|
6479
|
+
this.globalSettingsManager = options.globalSettingsManager;
|
|
6480
|
+
this.now = options.now ?? Date.now;
|
|
6481
|
+
this.localCacheDir = options.localCacheDir ?? getDefaultLocalModelCacheDir();
|
|
6482
|
+
this.localAssetStore = new LocalModelAssetStore({ indexPath: options.localAssetIndexPath ?? getDefaultLocalModelIndexPath() });
|
|
6483
|
+
this.localFetchCacheStore = new LocalModelFetchCacheStore({
|
|
6484
|
+
cachePath: options.localFetchCachePath ?? getDefaultLocalModelFetchCachePath(),
|
|
6485
|
+
now: this.now
|
|
6486
|
+
});
|
|
6487
|
+
}
|
|
6488
|
+
async listEngines() {
|
|
6489
|
+
const [config, globalSettings] = await Promise.all([this.configManager.readConfig(), this.globalSettingsManager.readSettings()]);
|
|
6490
|
+
return TRANSLATION_ENGINE_MANIFESTS.map((manifest) => ({
|
|
6491
|
+
...manifest,
|
|
6492
|
+
selected: config.translation.engineId === manifest.id,
|
|
6493
|
+
status: "available",
|
|
6494
|
+
model: manifest.id === "local" ? config.translation.engines.local.model ?? globalSettings.translationEngines.local.model : manifest.id === "openai" ? config.translation.engines.openai.model ?? globalSettings.translationEngines.openai.model : void 0
|
|
6495
|
+
}));
|
|
6496
|
+
}
|
|
6497
|
+
async searchModels(input) {
|
|
6498
|
+
if (input.engineId !== "local") return { items: [] };
|
|
6499
|
+
return searchLocalModels(input, { hfEndpoint: (await this.globalSettingsManager.readSettings()).translationEngines.local.hfEndpoint });
|
|
6500
|
+
}
|
|
6501
|
+
async getModelDownloadPlan(input) {
|
|
6502
|
+
if (input.engineId !== "local") return null;
|
|
6503
|
+
const state = (await this.localAssetStore.readMap()).get(input.model);
|
|
6504
|
+
const plan = await resolveLocalModelRuntimePlanFromProject({
|
|
6505
|
+
projectDir: this.projectDir,
|
|
6506
|
+
globalSettingsManager: this.globalSettingsManager,
|
|
6507
|
+
modelId: input.model,
|
|
6508
|
+
selectedGroupId: input.selectedGroupId,
|
|
6509
|
+
cacheDir: this.localCacheDir,
|
|
6510
|
+
fetchCacheStore: this.localFetchCacheStore,
|
|
6511
|
+
loadTransformersModule: this.loadLocalTransformersModuleForPlan.bind(this)
|
|
6512
|
+
}).catch(() => null);
|
|
6513
|
+
const fallbackPlan = selectPersistedLocalPlan(state, input.selectedGroupId);
|
|
6514
|
+
const effectivePlan = plan ?? fallbackPlan;
|
|
6515
|
+
if (!effectivePlan) return null;
|
|
6516
|
+
return enrichDownloadPlanWithAssetSnapshot(effectivePlan, state, input.selectedGroupId);
|
|
6517
|
+
}
|
|
6518
|
+
async selectEngine(engineId) {
|
|
6519
|
+
await this.configManager.writeConfig({ translation: { engineId } });
|
|
6520
|
+
return { success: true };
|
|
6521
|
+
}
|
|
6522
|
+
batchTranslate(input) {
|
|
6523
|
+
return observable((emit) => {
|
|
6524
|
+
if (input.engineId === "browser") {
|
|
6525
|
+
emit.error(/* @__PURE__ */ new Error("Browser translator runs in the browser runtime."));
|
|
6526
|
+
return () => {};
|
|
6527
|
+
}
|
|
6528
|
+
const controller = new AbortController();
|
|
6529
|
+
(async () => {
|
|
6530
|
+
try {
|
|
6531
|
+
if (input.engineId === "browser") throw new Error("Browser translator runs in the browser runtime.");
|
|
6532
|
+
const dtype = await this.readLocalDtype(input.engineId, input.model, input.selectedGroupId);
|
|
6533
|
+
if (input.engineId === "local" && input.model) await this.assertLocalModelReady(input.model, input.selectedGroupId);
|
|
6534
|
+
const translator = await (await this.loadFactory(input.engineId, input.model)).create({
|
|
6535
|
+
sourceLanguage: input.sourceLanguage,
|
|
6536
|
+
targetLanguage: input.targetLanguage,
|
|
6537
|
+
model: input.model,
|
|
6538
|
+
dtype,
|
|
6539
|
+
runtimeConfig: input.engineId === "local" && input.model ? await this.readLocalRuntimeConfig(input.model) : void 0,
|
|
6540
|
+
signal: controller.signal
|
|
6541
|
+
});
|
|
6542
|
+
try {
|
|
6543
|
+
for await (const event of translator.batchTranslate(input.inputs, {
|
|
6544
|
+
instructions: input.instructions,
|
|
6545
|
+
context: input.context,
|
|
6546
|
+
signal: controller.signal
|
|
6547
|
+
})) emit.next(event);
|
|
6548
|
+
emit.complete();
|
|
6549
|
+
} finally {
|
|
6550
|
+
translator.destroy?.();
|
|
6551
|
+
}
|
|
6552
|
+
} catch (error) {
|
|
6553
|
+
if (!controller.signal.aborted) emit.error(error instanceof Error ? error : new Error(String(error)));
|
|
6554
|
+
}
|
|
6555
|
+
})();
|
|
6556
|
+
return () => {
|
|
6557
|
+
controller.abort();
|
|
6558
|
+
};
|
|
6559
|
+
});
|
|
6560
|
+
}
|
|
6561
|
+
async readLocalDtype(engineId, model, selectedGroupId) {
|
|
6562
|
+
if (engineId !== "local" || !model) return void 0;
|
|
6563
|
+
const effectiveSelectedGroupId = selectedGroupId ?? (await this.globalSettingsManager.readSettings()).translationEngines.local.selectedGroupId;
|
|
6564
|
+
if (!effectiveSelectedGroupId) return void 0;
|
|
6565
|
+
return (await this.getModelDownloadPlan({
|
|
6566
|
+
engineId: "local",
|
|
6567
|
+
model,
|
|
6568
|
+
selectedGroupId: effectiveSelectedGroupId
|
|
6569
|
+
}))?.groups?.find((group) => group.id === effectiveSelectedGroupId)?.dtype;
|
|
6570
|
+
}
|
|
6571
|
+
async assertLocalModelReady(model, selectedGroupId) {
|
|
6572
|
+
const plan = await this.getModelDownloadPlan({
|
|
6573
|
+
engineId: "local",
|
|
6574
|
+
model,
|
|
6575
|
+
selectedGroupId
|
|
6576
|
+
});
|
|
6577
|
+
const files = (plan?.groups?.find((group) => group.id === (selectedGroupId ?? plan.selectedGroupId)) ?? plan?.groups?.find((group) => group.selected))?.files ?? plan?.files ?? [];
|
|
6578
|
+
if (!plan || files.length === 0) throw new Error("No local runtime file plan is available for the selected model.");
|
|
6579
|
+
const cacheStatus = await readLocalModelFileStatus({
|
|
6580
|
+
cacheDir: this.localCacheDir,
|
|
6581
|
+
modelId: model,
|
|
6582
|
+
files: files.map((file) => file.path)
|
|
6583
|
+
});
|
|
6584
|
+
if (cacheStatus.allCached) {
|
|
6585
|
+
const current = (await this.localAssetStore.readMap()).get(model);
|
|
6586
|
+
if (current) await this.localAssetStore.upsert({
|
|
6587
|
+
...current,
|
|
6588
|
+
status: "downloaded",
|
|
6589
|
+
progress: 1,
|
|
6590
|
+
bytesDownloaded: plan.estimatedTotalBytes ?? current.bytesDownloaded,
|
|
6591
|
+
totalBytes: plan.estimatedTotalBytes ?? current.totalBytes,
|
|
6592
|
+
resumable: false,
|
|
6593
|
+
error: void 0,
|
|
6594
|
+
plan,
|
|
6595
|
+
files: files.map((file) => ({
|
|
6596
|
+
path: file.path,
|
|
6597
|
+
sizeBytes: file.sizeBytes,
|
|
6598
|
+
downloadedBytes: file.sizeBytes
|
|
6599
|
+
})),
|
|
6600
|
+
installedAt: current.installedAt ?? this.now(),
|
|
6601
|
+
updatedAt: this.now()
|
|
6602
|
+
});
|
|
6603
|
+
return;
|
|
6604
|
+
}
|
|
6605
|
+
const allMissingFiles = cacheStatus.files.filter((file) => !file.cached).map((file) => file.file);
|
|
6606
|
+
const missingFiles = allMissingFiles.slice(0, 3);
|
|
6607
|
+
const suffix = allMissingFiles.length > missingFiles.length ? ` and ${allMissingFiles.length - missingFiles.length} more` : "";
|
|
6608
|
+
throw new Error(`Selected local model files are not installed locally: ${missingFiles.join(", ")}${suffix}.`);
|
|
6609
|
+
}
|
|
6610
|
+
async readLocalRuntimeConfig(model) {
|
|
6611
|
+
try {
|
|
6612
|
+
return JSON.parse(await readFile(join(this.localCacheDir, "models", model, "config.json"), "utf8"));
|
|
6613
|
+
} catch {
|
|
6614
|
+
return;
|
|
6615
|
+
}
|
|
6616
|
+
}
|
|
6617
|
+
async loadFactory(engineId, model) {
|
|
6618
|
+
const globalSettings = await this.globalSettingsManager.readSettings();
|
|
6619
|
+
if (engineId === "local") return (await import("@openspecui/local-translator")).createLocalTranslatorFactory({
|
|
6620
|
+
defaultModel: model ?? globalSettings.translationEngines.local.model,
|
|
6621
|
+
cacheDir: this.localCacheDir,
|
|
6622
|
+
localOnly: true
|
|
6623
|
+
});
|
|
6624
|
+
return (await import("@openspecui/openai-completion-translator")).createOpenAICompletionTranslatorFactory({
|
|
6625
|
+
baseUrl: globalSettings.translationEngines.openai.baseUrl,
|
|
6626
|
+
token: globalSettings.translationEngines.openai.token,
|
|
6627
|
+
model: model ?? globalSettings.translationEngines.openai.model
|
|
6628
|
+
});
|
|
6629
|
+
}
|
|
6630
|
+
async loadLocalTransformersModuleForPlan(_projectDir, _globalSettingsManager) {
|
|
6631
|
+
return await import("@huggingface/transformers");
|
|
6632
|
+
}
|
|
6633
|
+
};
|
|
6634
|
+
function enrichDownloadPlanWithAssetSnapshot(plan, state, selectedGroupId) {
|
|
6635
|
+
if (!state?.plan) return plan;
|
|
6636
|
+
const assetGroup = state.plan.groups?.find((group) => group.id === (selectedGroupId ?? plan.selectedGroupId));
|
|
6637
|
+
const mergedGroups = plan.groups?.map((group) => {
|
|
6638
|
+
const matchingAssetGroup = state.plan?.groups?.find((asset) => asset.id === group.id);
|
|
6639
|
+
if (!matchingAssetGroup) return group;
|
|
6640
|
+
return {
|
|
6641
|
+
...group,
|
|
6642
|
+
estimatedTotalBytes: group.estimatedTotalBytes ?? matchingAssetGroup.estimatedTotalBytes,
|
|
6643
|
+
files: group.files.map((file) => {
|
|
6644
|
+
const matchingAssetFile = matchingAssetGroup.files.find((asset) => asset.path === file.path);
|
|
6645
|
+
return matchingAssetFile?.sizeBytes !== void 0 && file.sizeBytes === void 0 ? {
|
|
6646
|
+
...file,
|
|
6647
|
+
sizeBytes: matchingAssetFile.sizeBytes
|
|
6648
|
+
} : file;
|
|
6649
|
+
})
|
|
6650
|
+
};
|
|
6651
|
+
});
|
|
6652
|
+
return {
|
|
6653
|
+
...plan,
|
|
6654
|
+
estimatedTotalBytes: plan.estimatedTotalBytes ?? assetGroup?.estimatedTotalBytes ?? state.plan.estimatedTotalBytes,
|
|
6655
|
+
groups: mergedGroups
|
|
6656
|
+
};
|
|
6657
|
+
}
|
|
6658
|
+
function selectPersistedLocalPlan(state, selectedGroupId) {
|
|
6659
|
+
const plan = state?.plan;
|
|
6660
|
+
if (!plan) return null;
|
|
6661
|
+
if (!selectedGroupId || !plan.groups?.length) return {
|
|
6662
|
+
...plan,
|
|
6663
|
+
files: [...plan.files],
|
|
6664
|
+
groups: plan.groups?.map((group) => ({
|
|
6665
|
+
...group,
|
|
6666
|
+
files: [...group.files]
|
|
6667
|
+
}))
|
|
6668
|
+
};
|
|
6669
|
+
const selectedGroup = plan.groups.find((group) => group.id === selectedGroupId);
|
|
6670
|
+
if (!selectedGroup) return null;
|
|
6671
|
+
return {
|
|
6672
|
+
modelId: plan.modelId,
|
|
6673
|
+
estimatedTotalBytes: selectedGroup.estimatedTotalBytes,
|
|
6674
|
+
files: [...selectedGroup.files],
|
|
6675
|
+
selectedGroupId: selectedGroup.id,
|
|
6676
|
+
groups: plan.groups.map((group) => ({
|
|
6677
|
+
...group,
|
|
6678
|
+
selected: group.id === selectedGroup.id,
|
|
6679
|
+
files: [...group.files]
|
|
6680
|
+
}))
|
|
6681
|
+
};
|
|
6682
|
+
}
|
|
6683
|
+
|
|
4571
6684
|
//#endregion
|
|
4572
6685
|
//#region src/workflow-invocation-service.ts
|
|
4573
6686
|
const COMMAND_CAPABLE_ACTIONS = new Set([
|
|
@@ -4822,7 +6935,7 @@ function deferBackgroundTask(task) {
|
|
|
4822
6935
|
function createServer(config) {
|
|
4823
6936
|
const adapter = new OpenSpecAdapter(config.projectDir);
|
|
4824
6937
|
const configManager = new ConfigManager(config.projectDir);
|
|
4825
|
-
const globalSettingsManager = new GlobalSettingsManager();
|
|
6938
|
+
const globalSettingsManager = new GlobalSettingsManager(config.runtimePaths?.globalSettingsPath);
|
|
4826
6939
|
const cliExecutor = new CliExecutor(configManager, config.projectDir);
|
|
4827
6940
|
const kernel = config.kernel;
|
|
4828
6941
|
const hookRuntime = createHookRuntime(config.projectDir);
|
|
@@ -4834,16 +6947,17 @@ function createServer(config) {
|
|
|
4834
6947
|
});
|
|
4835
6948
|
const notificationService = new NotificationService();
|
|
4836
6949
|
const customSoundService = new CustomSoundService();
|
|
6950
|
+
const translationCacheDatabasePath = config.runtimePaths?.translationCacheDatabasePath ?? getDefaultTranslationCacheDatabasePath();
|
|
4837
6951
|
let translationCacheAdapterPromise = null;
|
|
4838
6952
|
const getTranslationCacheAdapter = () => {
|
|
4839
|
-
translationCacheAdapterPromise ??= createRuntimeSqliteTranslationCacheAdapter(
|
|
6953
|
+
translationCacheAdapterPromise ??= createRuntimeSqliteTranslationCacheAdapter(translationCacheDatabasePath);
|
|
4840
6954
|
return translationCacheAdapterPromise;
|
|
4841
6955
|
};
|
|
4842
6956
|
const translationCacheService = new TranslationCacheService({
|
|
4843
6957
|
configManager,
|
|
4844
6958
|
globalSettingsManager,
|
|
4845
6959
|
adapter: {
|
|
4846
|
-
databasePath:
|
|
6960
|
+
databasePath: translationCacheDatabasePath,
|
|
4847
6961
|
init: async () => (await getTranslationCacheAdapter()).init(),
|
|
4848
6962
|
read: async (keyHash, now) => (await getTranslationCacheAdapter()).read(keyHash, now),
|
|
4849
6963
|
write: async (input, now) => (await getTranslationCacheAdapter()).write(input, now),
|
|
@@ -4859,6 +6973,25 @@ function createServer(config) {
|
|
|
4859
6973
|
console.warn("Translation cache write failed:", error);
|
|
4860
6974
|
}
|
|
4861
6975
|
});
|
|
6976
|
+
const nmtModelCacheDir = config.runtimePaths?.localModelCacheDir ?? getDefaultLocalModelCacheDir();
|
|
6977
|
+
const nmtModelIndexPath = config.runtimePaths?.localModelAssetIndexPath ?? getDefaultLocalModelIndexPath();
|
|
6978
|
+
const nmtModelFetchCachePath = config.runtimePaths?.localModelFetchCachePath ?? getDefaultLocalModelFetchCachePath();
|
|
6979
|
+
const translationEngineService = new TranslationEngineService({
|
|
6980
|
+
projectDir: config.projectDir,
|
|
6981
|
+
configManager,
|
|
6982
|
+
globalSettingsManager,
|
|
6983
|
+
localCacheDir: nmtModelCacheDir,
|
|
6984
|
+
localAssetIndexPath: nmtModelIndexPath,
|
|
6985
|
+
localFetchCachePath: nmtModelFetchCachePath
|
|
6986
|
+
});
|
|
6987
|
+
const localModelAssetService = new LocalModelAssetService({
|
|
6988
|
+
projectDir: config.projectDir,
|
|
6989
|
+
configManager,
|
|
6990
|
+
globalSettingsManager,
|
|
6991
|
+
cacheDir: nmtModelCacheDir,
|
|
6992
|
+
indexPath: nmtModelIndexPath,
|
|
6993
|
+
fetchCachePath: nmtModelFetchCachePath
|
|
6994
|
+
});
|
|
4862
6995
|
const watcher = config.enableWatcher !== false ? new OpenSpecWatcher(config.projectDir) : void 0;
|
|
4863
6996
|
const entityReadOptionsContext = {
|
|
4864
6997
|
adapter,
|
|
@@ -4940,6 +7073,8 @@ function createServer(config) {
|
|
|
4940
7073
|
customSoundService,
|
|
4941
7074
|
globalSettingsManager,
|
|
4942
7075
|
translationCacheService,
|
|
7076
|
+
translationEngineService,
|
|
7077
|
+
localModelAssetService,
|
|
4943
7078
|
gitWorktreeHandoff: config.gitWorktreeHandoff,
|
|
4944
7079
|
watcher,
|
|
4945
7080
|
projectDir: config.projectDir
|
|
@@ -4960,6 +7095,8 @@ function createServer(config) {
|
|
|
4960
7095
|
customSoundService,
|
|
4961
7096
|
globalSettingsManager,
|
|
4962
7097
|
translationCacheService,
|
|
7098
|
+
translationEngineService,
|
|
7099
|
+
localModelAssetService,
|
|
4963
7100
|
gitWorktreeHandoff: config.gitWorktreeHandoff,
|
|
4964
7101
|
watcher,
|
|
4965
7102
|
projectDir: config.projectDir
|
|
@@ -4979,6 +7116,7 @@ function createServer(config) {
|
|
|
4979
7116
|
customSoundService,
|
|
4980
7117
|
globalSettingsManager,
|
|
4981
7118
|
translationCacheService,
|
|
7119
|
+
translationEngineService,
|
|
4982
7120
|
hookRuntime,
|
|
4983
7121
|
watcher,
|
|
4984
7122
|
createContext,
|
|
@@ -5077,6 +7215,7 @@ async function startServer(config, setupApp) {
|
|
|
5077
7215
|
close: async () => {
|
|
5078
7216
|
kernel.dispose();
|
|
5079
7217
|
await server.hookRuntime.dispose();
|
|
7218
|
+
await server.createContext().localModelAssetService.close();
|
|
5080
7219
|
server.translationCacheService.close();
|
|
5081
7220
|
wsServer.close();
|
|
5082
7221
|
httpServer.close();
|
|
@@ -5085,4 +7224,4 @@ async function startServer(config, setupApp) {
|
|
|
5085
7224
|
}
|
|
5086
7225
|
|
|
5087
7226
|
//#endregion
|
|
5088
|
-
export { DocumentService, OPENSPECUI_HOOKS_RELATIVE_PATH, ProjectHookRuntime, WorkflowInvocationService, createHookRuntime, createServer, createWebSocketServer, findAvailablePort, isPortAvailable, startServer };
|
|
7227
|
+
export { DocumentService, LocalModelAssetService, OPENSPECUI_HOOKS_RELATIVE_PATH, ProjectHookRuntime, TranslationEngineService, WorkflowInvocationService, createHookRuntime, createServer, createWebSocketServer, findAvailablePort, isPortAvailable, startServer };
|