@openspecui/server 3.9.0 → 3.11.0

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