noosphere 0.4.1 → 0.7.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/README.md +227 -19
- package/dist/index.cjs +1083 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +149 -4
- package/dist/index.d.ts +149 -4
- package/dist/index.js +1077 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -2
- package/assets/logos/png/ai21.png +0 -0
- package/assets/logos/png/amazon.png +0 -0
- package/assets/logos/png/anthropic.png +0 -0
- package/assets/logos/png/baidu.png +0 -0
- package/assets/logos/png/bytedance.png +0 -0
- package/assets/logos/png/cerebras.png +0 -0
- package/assets/logos/png/cohere.png +0 -0
- package/assets/logos/png/comfyui.png +0 -0
- package/assets/logos/png/deepseek.png +0 -0
- package/assets/logos/png/fal.png +0 -0
- package/assets/logos/png/fireworks-ai.png +0 -0
- package/assets/logos/png/google.png +0 -0
- package/assets/logos/png/groq.png +0 -0
- package/assets/logos/png/huggingface.png +0 -0
- package/assets/logos/png/ibm.png +0 -0
- package/assets/logos/png/inflection.png +0 -0
- package/assets/logos/png/kokoro.png +0 -0
- package/assets/logos/png/meta.png +0 -0
- package/assets/logos/png/microsoft.png +0 -0
- package/assets/logos/png/minimax.png +0 -0
- package/assets/logos/png/mistral.png +0 -0
- package/assets/logos/png/nebius.png +0 -0
- package/assets/logos/png/novita.png +0 -0
- package/assets/logos/png/nvidia.png +0 -0
- package/assets/logos/png/ollama.png +0 -0
- package/assets/logos/png/openai.png +0 -0
- package/assets/logos/png/openrouter.png +0 -0
- package/assets/logos/png/perplexity.png +0 -0
- package/assets/logos/png/pi-ai.png +0 -0
- package/assets/logos/png/piper.png +0 -0
- package/assets/logos/png/qwen.png +0 -0
- package/assets/logos/png/replicate.png +0 -0
- package/assets/logos/png/sambanova.png +0 -0
- package/assets/logos/png/tencent.png +0 -0
- package/assets/logos/png/together.png +0 -0
- package/assets/logos/png/upstage.png +0 -0
- package/assets/logos/png/xai.png +0 -0
- package/assets/logos/png/xiaomi.png +0 -0
- package/assets/logos/png/zai.png +0 -0
- package/assets/logos/svg/amazon.svg +0 -1
- package/assets/logos/svg/anthropic.svg +0 -1
- package/assets/logos/svg/baidu.svg +0 -1
- package/assets/logos/svg/cerebras.svg +0 -1
- package/assets/logos/svg/cohere.svg +0 -1
- package/assets/logos/svg/deepseek.svg +0 -1
- package/assets/logos/svg/fireworks-ai.svg +0 -9
- package/assets/logos/svg/google.svg +0 -1
- package/assets/logos/svg/groq.svg +0 -1
- package/assets/logos/svg/huggingface.svg +0 -67
- package/assets/logos/svg/meta.svg +0 -1
- package/assets/logos/svg/microsoft.svg +0 -1
- package/assets/logos/svg/mistral.svg +0 -1
- package/assets/logos/svg/nebius.svg +0 -4
- package/assets/logos/svg/novita.svg +0 -11
- package/assets/logos/svg/nvidia.svg +0 -1
- package/assets/logos/svg/ollama.svg +0 -7
- package/assets/logos/svg/openai.svg +0 -1
- package/assets/logos/svg/openrouter.svg +0 -21
- package/assets/logos/svg/perplexity.svg +0 -24
- package/assets/logos/svg/qwen.svg +0 -1
- package/assets/logos/svg/replicate.svg +0 -12
- package/assets/logos/svg/together.svg +0 -1
- package/assets/logos/svg/xai.svg +0 -1
package/dist/index.cjs
CHANGED
|
@@ -20,10 +20,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
AudioCraftProvider: () => AudioCraftProvider,
|
|
24
|
+
HfLocalProvider: () => HfLocalProvider,
|
|
23
25
|
Noosphere: () => Noosphere,
|
|
24
26
|
NoosphereError: () => NoosphereError,
|
|
27
|
+
OllamaProvider: () => OllamaProvider,
|
|
28
|
+
OpenAICompatProvider: () => OpenAICompatProvider,
|
|
25
29
|
PROVIDER_IDS: () => PROVIDER_IDS,
|
|
26
30
|
PROVIDER_LOGOS: () => PROVIDER_LOGOS,
|
|
31
|
+
WhisperLocalProvider: () => WhisperLocalProvider,
|
|
32
|
+
detectOpenAICompatServers: () => detectOpenAICompatServers,
|
|
27
33
|
getAllProviderLogos: () => getAllProviderLogos,
|
|
28
34
|
getProviderLogo: () => getProviderLogo
|
|
29
35
|
});
|
|
@@ -133,21 +139,7 @@ function resolveConfig(input) {
|
|
|
133
139
|
}
|
|
134
140
|
|
|
135
141
|
// src/logos.ts
|
|
136
|
-
var
|
|
137
|
-
var import_path = require("path");
|
|
138
|
-
var import_fs = require("fs");
|
|
139
|
-
var import_meta = {};
|
|
140
|
-
var _assetsDir = null;
|
|
141
|
-
function assetsDir() {
|
|
142
|
-
if (_assetsDir) return _assetsDir;
|
|
143
|
-
try {
|
|
144
|
-
const __filename = (0, import_url.fileURLToPath)(import_meta.url);
|
|
145
|
-
_assetsDir = (0, import_path.resolve)((0, import_path.dirname)(__filename), "..", "assets", "logos");
|
|
146
|
-
} catch {
|
|
147
|
-
_assetsDir = (0, import_path.resolve)(__dirname, "..", "assets", "logos");
|
|
148
|
-
}
|
|
149
|
-
return _assetsDir;
|
|
150
|
-
}
|
|
142
|
+
var CDN_BASE = "https://blockchainstarter.nyc3.digitaloceanspaces.com/noosphere/logos";
|
|
151
143
|
var PROVIDER_IDS = [
|
|
152
144
|
// Cloud LLM
|
|
153
145
|
"openai",
|
|
@@ -195,6 +187,40 @@ var PROVIDER_IDS = [
|
|
|
195
187
|
"nebius",
|
|
196
188
|
"novita"
|
|
197
189
|
];
|
|
190
|
+
var HAS_SVG = /* @__PURE__ */ new Set([
|
|
191
|
+
"openai",
|
|
192
|
+
"anthropic",
|
|
193
|
+
"google",
|
|
194
|
+
"groq",
|
|
195
|
+
"mistral",
|
|
196
|
+
"xai",
|
|
197
|
+
"openrouter",
|
|
198
|
+
"cerebras",
|
|
199
|
+
"huggingface",
|
|
200
|
+
"ollama",
|
|
201
|
+
"meta",
|
|
202
|
+
"deepseek",
|
|
203
|
+
"microsoft",
|
|
204
|
+
"nvidia",
|
|
205
|
+
"qwen",
|
|
206
|
+
"cohere",
|
|
207
|
+
"perplexity",
|
|
208
|
+
"amazon",
|
|
209
|
+
"baidu",
|
|
210
|
+
"together",
|
|
211
|
+
"fireworks-ai",
|
|
212
|
+
"replicate",
|
|
213
|
+
"nebius",
|
|
214
|
+
"novita",
|
|
215
|
+
"comfyui",
|
|
216
|
+
"fal",
|
|
217
|
+
"kokoro",
|
|
218
|
+
"piper",
|
|
219
|
+
"sambanova",
|
|
220
|
+
"pi-ai",
|
|
221
|
+
"zai"
|
|
222
|
+
// NO SVG: bytedance, tencent, xiaomi, ibm, ai21, inflection, upstage, minimax
|
|
223
|
+
]);
|
|
198
224
|
var _cache = /* @__PURE__ */ new Map();
|
|
199
225
|
function getProviderLogo(providerId) {
|
|
200
226
|
if (!providerId) return void 0;
|
|
@@ -217,13 +243,12 @@ function getProviderLogo(providerId) {
|
|
|
217
243
|
}
|
|
218
244
|
}
|
|
219
245
|
if (!matchedId) return void 0;
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (!logo.svg && !logo.png) return void 0;
|
|
246
|
+
const logo = {
|
|
247
|
+
png: `${CDN_BASE}/png/${matchedId}.png`
|
|
248
|
+
};
|
|
249
|
+
if (HAS_SVG.has(matchedId)) {
|
|
250
|
+
logo.svg = `${CDN_BASE}/svg/${matchedId}.svg`;
|
|
251
|
+
}
|
|
227
252
|
_cache.set(providerId, logo);
|
|
228
253
|
return logo;
|
|
229
254
|
}
|
|
@@ -399,7 +424,7 @@ var UsageTracker = class {
|
|
|
399
424
|
filtered = filtered.filter((e) => e.modality === options.modality);
|
|
400
425
|
}
|
|
401
426
|
const byProvider = {};
|
|
402
|
-
const byModality = { llm: 0, image: 0, video: 0, tts: 0 };
|
|
427
|
+
const byModality = { llm: 0, image: 0, video: 0, tts: 0, stt: 0, music: 0, embedding: 0 };
|
|
403
428
|
let totalCost = 0;
|
|
404
429
|
for (const event of filtered) {
|
|
405
430
|
totalCost += event.cost;
|
|
@@ -626,8 +651,8 @@ var PiAiProvider = class {
|
|
|
626
651
|
let aborted = false;
|
|
627
652
|
let resolveResult = null;
|
|
628
653
|
let rejectResult = null;
|
|
629
|
-
const resultPromise = new Promise((
|
|
630
|
-
resolveResult =
|
|
654
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
655
|
+
resolveResult = resolve;
|
|
631
656
|
rejectResult = reject;
|
|
632
657
|
});
|
|
633
658
|
const ensureModel = async () => {
|
|
@@ -1024,11 +1049,55 @@ var ComfyUIProvider = class {
|
|
|
1024
1049
|
}
|
|
1025
1050
|
}
|
|
1026
1051
|
async listModels(modality) {
|
|
1052
|
+
const models = [];
|
|
1053
|
+
const logo = getProviderLogo("comfyui");
|
|
1027
1054
|
try {
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1055
|
+
const controller = new AbortController();
|
|
1056
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
1057
|
+
try {
|
|
1058
|
+
const res = await fetch(`${this.baseUrl}/object_info`, { signal: controller.signal });
|
|
1059
|
+
if (res.ok) {
|
|
1060
|
+
const objectInfo = await res.json();
|
|
1061
|
+
const ckptNode = objectInfo?.["CheckpointLoaderSimple"];
|
|
1062
|
+
const ckptNames = ckptNode?.input?.required?.ckpt_name?.[0] ?? [];
|
|
1063
|
+
for (const name of ckptNames) {
|
|
1064
|
+
if (modality && modality !== "image") continue;
|
|
1065
|
+
models.push({
|
|
1066
|
+
id: `comfyui-ckpt-${name}`,
|
|
1067
|
+
provider: "comfyui",
|
|
1068
|
+
name: `ComfyUI: ${name}`,
|
|
1069
|
+
modality: "image",
|
|
1070
|
+
local: true,
|
|
1071
|
+
cost: { price: 0, unit: "free" },
|
|
1072
|
+
logo,
|
|
1073
|
+
status: "installed",
|
|
1074
|
+
localInfo: { sizeBytes: 0, runtime: "comfyui" },
|
|
1075
|
+
capabilities: { maxWidth: 2048, maxHeight: 2048, supportsNegativePrompt: true }
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
const loraNode = objectInfo?.["LoraLoader"];
|
|
1079
|
+
const loraNames = loraNode?.input?.required?.lora_name?.[0] ?? [];
|
|
1080
|
+
for (const name of loraNames) {
|
|
1081
|
+
if (modality && modality !== "image") continue;
|
|
1082
|
+
models.push({
|
|
1083
|
+
id: `comfyui-lora-${name}`,
|
|
1084
|
+
provider: "comfyui",
|
|
1085
|
+
name: `LoRA: ${name}`,
|
|
1086
|
+
modality: "image",
|
|
1087
|
+
local: true,
|
|
1088
|
+
cost: { price: 0, unit: "free" },
|
|
1089
|
+
logo,
|
|
1090
|
+
status: "installed",
|
|
1091
|
+
localInfo: { sizeBytes: 0, runtime: "comfyui" }
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
} finally {
|
|
1096
|
+
clearTimeout(timer);
|
|
1097
|
+
}
|
|
1098
|
+
} catch {
|
|
1099
|
+
}
|
|
1100
|
+
if (models.length === 0) {
|
|
1032
1101
|
if (!modality || modality === "image") {
|
|
1033
1102
|
models.push({
|
|
1034
1103
|
id: "comfyui-txt2img",
|
|
@@ -1053,10 +1122,43 @@ var ComfyUIProvider = class {
|
|
|
1053
1122
|
capabilities: { maxDuration: 10, supportsImageToVideo: true }
|
|
1054
1123
|
});
|
|
1055
1124
|
}
|
|
1056
|
-
return models;
|
|
1057
|
-
} catch {
|
|
1058
|
-
return [];
|
|
1059
1125
|
}
|
|
1126
|
+
if (!modality || modality === "image") {
|
|
1127
|
+
try {
|
|
1128
|
+
const controller = new AbortController();
|
|
1129
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
1130
|
+
try {
|
|
1131
|
+
const res = await fetch(
|
|
1132
|
+
"https://civitai.com/api/v1/models?types=Checkpoint&sort=Highest%20Rated&limit=50&nsfw=false",
|
|
1133
|
+
{ signal: controller.signal }
|
|
1134
|
+
);
|
|
1135
|
+
if (res.ok) {
|
|
1136
|
+
const data = await res.json();
|
|
1137
|
+
for (const item of data.items ?? []) {
|
|
1138
|
+
const version = item.modelVersions?.[0];
|
|
1139
|
+
models.push({
|
|
1140
|
+
id: `civitai-${item.id}`,
|
|
1141
|
+
provider: "comfyui",
|
|
1142
|
+
name: item.name ?? `CivitAI Model ${item.id}`,
|
|
1143
|
+
modality: "image",
|
|
1144
|
+
local: true,
|
|
1145
|
+
cost: { price: 0, unit: "free" },
|
|
1146
|
+
logo,
|
|
1147
|
+
status: "available",
|
|
1148
|
+
localInfo: {
|
|
1149
|
+
sizeBytes: version?.files?.[0]?.sizeKB ? version.files[0].sizeKB * 1024 : 0,
|
|
1150
|
+
runtime: "comfyui"
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
} finally {
|
|
1156
|
+
clearTimeout(timer);
|
|
1157
|
+
}
|
|
1158
|
+
} catch {
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return models;
|
|
1060
1162
|
}
|
|
1061
1163
|
async image(options) {
|
|
1062
1164
|
const start = Date.now();
|
|
@@ -1396,6 +1498,890 @@ var HuggingFaceProvider = class {
|
|
|
1396
1498
|
}
|
|
1397
1499
|
};
|
|
1398
1500
|
|
|
1501
|
+
// src/providers/ollama.ts
|
|
1502
|
+
var OLLAMA_FAMILY_TO_PROVIDER = {
|
|
1503
|
+
"llama": "meta",
|
|
1504
|
+
"codellama": "meta",
|
|
1505
|
+
"gemma": "google",
|
|
1506
|
+
"gemma2": "google",
|
|
1507
|
+
"gemma3": "google",
|
|
1508
|
+
"qwen": "qwen",
|
|
1509
|
+
"qwen2": "qwen",
|
|
1510
|
+
"qwen2.5": "qwen",
|
|
1511
|
+
"qwen3": "qwen",
|
|
1512
|
+
"deepseek": "deepseek",
|
|
1513
|
+
"deepcoder": "deepseek",
|
|
1514
|
+
"deepscaler": "deepseek",
|
|
1515
|
+
"qwq": "qwen",
|
|
1516
|
+
"phi": "microsoft",
|
|
1517
|
+
"phi3": "microsoft",
|
|
1518
|
+
"phi4": "microsoft",
|
|
1519
|
+
"mistral": "mistral",
|
|
1520
|
+
"mixtral": "mistral",
|
|
1521
|
+
"codestral": "mistral",
|
|
1522
|
+
"ministral": "mistral",
|
|
1523
|
+
"nemotron": "nvidia",
|
|
1524
|
+
"command": "cohere",
|
|
1525
|
+
"command-r": "cohere",
|
|
1526
|
+
"gpt-oss": "openai",
|
|
1527
|
+
"starcoder": "huggingface",
|
|
1528
|
+
"falcon": "meta",
|
|
1529
|
+
"glm": "zai",
|
|
1530
|
+
"granite": "ibm",
|
|
1531
|
+
"olmo": "meta",
|
|
1532
|
+
"yi": "zai",
|
|
1533
|
+
"minimax": "minimax",
|
|
1534
|
+
"kimi": "meta",
|
|
1535
|
+
"dolphin": "ollama",
|
|
1536
|
+
"wizard": "ollama",
|
|
1537
|
+
"nomic": "ollama",
|
|
1538
|
+
"mxbai": "ollama",
|
|
1539
|
+
"bge": "ollama",
|
|
1540
|
+
"all-minilm": "ollama",
|
|
1541
|
+
"moondream": "ollama"
|
|
1542
|
+
};
|
|
1543
|
+
var VISION_MODELS = /* @__PURE__ */ new Set([
|
|
1544
|
+
"llava",
|
|
1545
|
+
"moondream",
|
|
1546
|
+
"minicpm-v",
|
|
1547
|
+
"llama3.2-vision",
|
|
1548
|
+
"qwen2.5vl",
|
|
1549
|
+
"gemma3",
|
|
1550
|
+
"llava-llama3",
|
|
1551
|
+
"llava-phi3",
|
|
1552
|
+
"bakllava"
|
|
1553
|
+
]);
|
|
1554
|
+
function inferLogoProvider(modelName, _family) {
|
|
1555
|
+
const base = modelName.split(":")[0].toLowerCase().replace(/^[^/]+\//, "");
|
|
1556
|
+
const sortedPrefixes = Object.entries(OLLAMA_FAMILY_TO_PROVIDER).sort((a, b) => b[0].length - a[0].length);
|
|
1557
|
+
for (const [prefix, provider] of sortedPrefixes) {
|
|
1558
|
+
if (base === prefix || base.startsWith(prefix)) return provider;
|
|
1559
|
+
}
|
|
1560
|
+
return "ollama";
|
|
1561
|
+
}
|
|
1562
|
+
function supportsVision(modelName) {
|
|
1563
|
+
const base = modelName.split(":")[0].toLowerCase();
|
|
1564
|
+
for (const v of VISION_MODELS) {
|
|
1565
|
+
if (base === v || base.startsWith(v)) return true;
|
|
1566
|
+
}
|
|
1567
|
+
return false;
|
|
1568
|
+
}
|
|
1569
|
+
async function fetchJson(url, options) {
|
|
1570
|
+
const timeoutMs = options?.timeoutMs ?? 5e3;
|
|
1571
|
+
const controller = new AbortController();
|
|
1572
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1573
|
+
try {
|
|
1574
|
+
const res = await fetch(url, { ...options, signal: controller.signal });
|
|
1575
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1576
|
+
return await res.json();
|
|
1577
|
+
} finally {
|
|
1578
|
+
clearTimeout(timer);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
var OllamaProvider = class {
|
|
1582
|
+
id = "ollama";
|
|
1583
|
+
name = "Ollama (Local)";
|
|
1584
|
+
modalities = ["llm"];
|
|
1585
|
+
isLocal = true;
|
|
1586
|
+
baseUrl;
|
|
1587
|
+
constructor(config) {
|
|
1588
|
+
const host = config?.host ?? "http://localhost";
|
|
1589
|
+
const port = config?.port ?? 11434;
|
|
1590
|
+
const cleanHost = host.replace(/\/+$/, "");
|
|
1591
|
+
const hasPort = /:\d+$/.test(cleanHost);
|
|
1592
|
+
this.baseUrl = hasPort ? cleanHost : `${cleanHost}:${port}`;
|
|
1593
|
+
}
|
|
1594
|
+
async ping() {
|
|
1595
|
+
try {
|
|
1596
|
+
await fetchJson(`${this.baseUrl}/api/version`, { timeoutMs: 2e3 });
|
|
1597
|
+
return true;
|
|
1598
|
+
} catch {
|
|
1599
|
+
return false;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
async listModels(_modality) {
|
|
1603
|
+
if (_modality && _modality !== "llm") return [];
|
|
1604
|
+
const [localData, catalogData, runningData] = await Promise.all([
|
|
1605
|
+
fetchJson(`${this.baseUrl}/api/tags`, { timeoutMs: 5e3 }).catch(() => null),
|
|
1606
|
+
fetchJson("https://ollama.com/api/tags", { timeoutMs: 5e3 }).catch(() => null),
|
|
1607
|
+
fetchJson(`${this.baseUrl}/api/ps`, { timeoutMs: 5e3 }).catch(() => null)
|
|
1608
|
+
]);
|
|
1609
|
+
const runningNames = /* @__PURE__ */ new Set();
|
|
1610
|
+
if (runningData?.models) {
|
|
1611
|
+
for (const m of runningData.models) {
|
|
1612
|
+
runningNames.add(m.name);
|
|
1613
|
+
runningNames.add(m.model);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
const models = /* @__PURE__ */ new Map();
|
|
1617
|
+
if (localData?.models) {
|
|
1618
|
+
for (const m of localData.models) {
|
|
1619
|
+
const isRunning = runningNames.has(m.name) || runningNames.has(m.model);
|
|
1620
|
+
models.set(m.name, this.toModelInfo(m, isRunning ? "running" : "installed", true));
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
if (catalogData?.models) {
|
|
1624
|
+
for (const m of catalogData.models) {
|
|
1625
|
+
const name = m.name;
|
|
1626
|
+
if (!models.has(name)) {
|
|
1627
|
+
models.set(name, this.toModelInfo(m, "available", false));
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
return Array.from(models.values());
|
|
1632
|
+
}
|
|
1633
|
+
toModelInfo(m, status, isLocal) {
|
|
1634
|
+
const name = m.name ?? m.model ?? "unknown";
|
|
1635
|
+
const family = m.details?.family;
|
|
1636
|
+
const logoProvider = inferLogoProvider(name, family);
|
|
1637
|
+
return {
|
|
1638
|
+
id: name,
|
|
1639
|
+
provider: "ollama",
|
|
1640
|
+
name,
|
|
1641
|
+
modality: "llm",
|
|
1642
|
+
local: true,
|
|
1643
|
+
cost: { price: 0, unit: "free" },
|
|
1644
|
+
logo: getProviderLogo(logoProvider),
|
|
1645
|
+
status,
|
|
1646
|
+
localInfo: {
|
|
1647
|
+
sizeBytes: m.size ?? 0,
|
|
1648
|
+
family: family ?? m.details?.family,
|
|
1649
|
+
parameterSize: m.details?.parameter_size,
|
|
1650
|
+
quantization: m.details?.quantization_level,
|
|
1651
|
+
format: m.details?.format,
|
|
1652
|
+
digest: m.digest,
|
|
1653
|
+
modifiedAt: m.modified_at,
|
|
1654
|
+
running: status === "running",
|
|
1655
|
+
runtime: "ollama"
|
|
1656
|
+
},
|
|
1657
|
+
capabilities: {
|
|
1658
|
+
contextWindow: 128e3,
|
|
1659
|
+
supportsVision: supportsVision(name),
|
|
1660
|
+
supportsStreaming: true
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
async chat(options) {
|
|
1665
|
+
const start = Date.now();
|
|
1666
|
+
const messages = options.messages.map((m) => ({
|
|
1667
|
+
role: m.role,
|
|
1668
|
+
content: m.content
|
|
1669
|
+
}));
|
|
1670
|
+
const body = {
|
|
1671
|
+
model: options.model ?? "llama3.2",
|
|
1672
|
+
messages,
|
|
1673
|
+
stream: false
|
|
1674
|
+
};
|
|
1675
|
+
if (options.temperature !== void 0 || options.maxTokens !== void 0) {
|
|
1676
|
+
body.options = {};
|
|
1677
|
+
if (options.temperature !== void 0) body.options.temperature = options.temperature;
|
|
1678
|
+
if (options.maxTokens !== void 0) body.options.num_predict = options.maxTokens;
|
|
1679
|
+
}
|
|
1680
|
+
const res = await fetch(`${this.baseUrl}/api/chat`, {
|
|
1681
|
+
method: "POST",
|
|
1682
|
+
headers: { "Content-Type": "application/json" },
|
|
1683
|
+
body: JSON.stringify(body)
|
|
1684
|
+
});
|
|
1685
|
+
if (!res.ok) {
|
|
1686
|
+
throw new Error(`Ollama chat failed: ${res.status} ${await res.text()}`);
|
|
1687
|
+
}
|
|
1688
|
+
const data = await res.json();
|
|
1689
|
+
return {
|
|
1690
|
+
content: data.message?.content ?? "",
|
|
1691
|
+
provider: "ollama",
|
|
1692
|
+
model: options.model ?? "llama3.2",
|
|
1693
|
+
modality: "llm",
|
|
1694
|
+
latencyMs: Date.now() - start,
|
|
1695
|
+
usage: {
|
|
1696
|
+
cost: 0,
|
|
1697
|
+
input: data.prompt_eval_count ?? 0,
|
|
1698
|
+
output: data.eval_count ?? 0,
|
|
1699
|
+
unit: "tokens"
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
stream(options) {
|
|
1704
|
+
const self = this;
|
|
1705
|
+
const start = Date.now();
|
|
1706
|
+
let aborted = false;
|
|
1707
|
+
let resolveResult = null;
|
|
1708
|
+
let rejectResult = null;
|
|
1709
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
1710
|
+
resolveResult = resolve;
|
|
1711
|
+
rejectResult = reject;
|
|
1712
|
+
});
|
|
1713
|
+
const messages = options.messages.map((m) => ({
|
|
1714
|
+
role: m.role,
|
|
1715
|
+
content: m.content
|
|
1716
|
+
}));
|
|
1717
|
+
const body = {
|
|
1718
|
+
model: options.model ?? "llama3.2",
|
|
1719
|
+
messages,
|
|
1720
|
+
stream: true
|
|
1721
|
+
};
|
|
1722
|
+
if (options.temperature !== void 0 || options.maxTokens !== void 0) {
|
|
1723
|
+
body.options = {};
|
|
1724
|
+
if (options.temperature !== void 0) body.options.temperature = options.temperature;
|
|
1725
|
+
if (options.maxTokens !== void 0) body.options.num_predict = options.maxTokens;
|
|
1726
|
+
}
|
|
1727
|
+
const asyncIterator = {
|
|
1728
|
+
async *[Symbol.asyncIterator]() {
|
|
1729
|
+
try {
|
|
1730
|
+
const res = await fetch(`${self.baseUrl}/api/chat`, {
|
|
1731
|
+
method: "POST",
|
|
1732
|
+
headers: { "Content-Type": "application/json" },
|
|
1733
|
+
body: JSON.stringify(body)
|
|
1734
|
+
});
|
|
1735
|
+
if (!res.ok) {
|
|
1736
|
+
throw new Error(`Ollama stream failed: ${res.status} ${await res.text()}`);
|
|
1737
|
+
}
|
|
1738
|
+
const reader = res.body.getReader();
|
|
1739
|
+
const decoder = new TextDecoder();
|
|
1740
|
+
let fullContent = "";
|
|
1741
|
+
let finalData = null;
|
|
1742
|
+
let buffer = "";
|
|
1743
|
+
while (!aborted) {
|
|
1744
|
+
const { done, value } = await reader.read();
|
|
1745
|
+
if (done) break;
|
|
1746
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1747
|
+
const lines = buffer.split("\n");
|
|
1748
|
+
buffer = lines.pop() ?? "";
|
|
1749
|
+
for (const line of lines) {
|
|
1750
|
+
if (!line.trim()) continue;
|
|
1751
|
+
try {
|
|
1752
|
+
const chunk = JSON.parse(line);
|
|
1753
|
+
if (chunk.message?.content) {
|
|
1754
|
+
fullContent += chunk.message.content;
|
|
1755
|
+
yield { type: "text_delta", delta: chunk.message.content };
|
|
1756
|
+
}
|
|
1757
|
+
if (chunk.done) {
|
|
1758
|
+
finalData = chunk;
|
|
1759
|
+
}
|
|
1760
|
+
} catch {
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
const result = {
|
|
1765
|
+
content: fullContent,
|
|
1766
|
+
provider: "ollama",
|
|
1767
|
+
model: options.model ?? "llama3.2",
|
|
1768
|
+
modality: "llm",
|
|
1769
|
+
latencyMs: Date.now() - start,
|
|
1770
|
+
usage: {
|
|
1771
|
+
cost: 0,
|
|
1772
|
+
input: finalData?.prompt_eval_count ?? 0,
|
|
1773
|
+
output: finalData?.eval_count ?? 0,
|
|
1774
|
+
unit: "tokens"
|
|
1775
|
+
}
|
|
1776
|
+
};
|
|
1777
|
+
resolveResult?.(result);
|
|
1778
|
+
yield { type: "done", result };
|
|
1779
|
+
} catch (err) {
|
|
1780
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1781
|
+
rejectResult?.(error);
|
|
1782
|
+
yield { type: "error", error };
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
};
|
|
1786
|
+
return {
|
|
1787
|
+
[Symbol.asyncIterator]: () => asyncIterator[Symbol.asyncIterator](),
|
|
1788
|
+
result: () => resultPromise,
|
|
1789
|
+
abort: () => {
|
|
1790
|
+
aborted = true;
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
// --- Extra model management methods ---
|
|
1795
|
+
async *pullModel(name) {
|
|
1796
|
+
const res = await fetch(`${this.baseUrl}/api/pull`, {
|
|
1797
|
+
method: "POST",
|
|
1798
|
+
headers: { "Content-Type": "application/json" },
|
|
1799
|
+
body: JSON.stringify({ name, stream: true })
|
|
1800
|
+
});
|
|
1801
|
+
if (!res.ok) {
|
|
1802
|
+
throw new Error(`Ollama pull failed: ${res.status} ${await res.text()}`);
|
|
1803
|
+
}
|
|
1804
|
+
const reader = res.body.getReader();
|
|
1805
|
+
const decoder = new TextDecoder();
|
|
1806
|
+
let buffer = "";
|
|
1807
|
+
while (true) {
|
|
1808
|
+
const { done, value } = await reader.read();
|
|
1809
|
+
if (done) break;
|
|
1810
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1811
|
+
const lines = buffer.split("\n");
|
|
1812
|
+
buffer = lines.pop() ?? "";
|
|
1813
|
+
for (const line of lines) {
|
|
1814
|
+
if (!line.trim()) continue;
|
|
1815
|
+
try {
|
|
1816
|
+
yield JSON.parse(line);
|
|
1817
|
+
} catch {
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
async deleteModel(name) {
|
|
1823
|
+
const res = await fetch(`${this.baseUrl}/api/delete`, {
|
|
1824
|
+
method: "DELETE",
|
|
1825
|
+
headers: { "Content-Type": "application/json" },
|
|
1826
|
+
body: JSON.stringify({ name })
|
|
1827
|
+
});
|
|
1828
|
+
if (!res.ok) {
|
|
1829
|
+
throw new Error(`Ollama delete failed: ${res.status} ${await res.text()}`);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
async showModel(name) {
|
|
1833
|
+
const res = await fetch(`${this.baseUrl}/api/show`, {
|
|
1834
|
+
method: "POST",
|
|
1835
|
+
headers: { "Content-Type": "application/json" },
|
|
1836
|
+
body: JSON.stringify({ name })
|
|
1837
|
+
});
|
|
1838
|
+
if (!res.ok) {
|
|
1839
|
+
throw new Error(`Ollama show failed: ${res.status} ${await res.text()}`);
|
|
1840
|
+
}
|
|
1841
|
+
return await res.json();
|
|
1842
|
+
}
|
|
1843
|
+
async getRunningModels() {
|
|
1844
|
+
const data = await fetchJson(`${this.baseUrl}/api/ps`, { timeoutMs: 5e3 });
|
|
1845
|
+
return data?.models ?? [];
|
|
1846
|
+
}
|
|
1847
|
+
};
|
|
1848
|
+
|
|
1849
|
+
// src/providers/hf-local.ts
|
|
1850
|
+
var import_promises = require("fs/promises");
|
|
1851
|
+
var import_node_path = require("path");
|
|
1852
|
+
var import_node_os = require("os");
|
|
1853
|
+
var FETCH_TIMEOUT_MS3 = 5e3;
|
|
1854
|
+
var HF_HUB_API2 = "https://huggingface.co/api/models";
|
|
1855
|
+
var PIPELINE_TAG_TO_MODALITY = {
|
|
1856
|
+
"text-to-image": "image",
|
|
1857
|
+
"text-to-video": "video",
|
|
1858
|
+
"text-to-audio": "tts",
|
|
1859
|
+
"text-to-speech": "tts",
|
|
1860
|
+
"automatic-speech-recognition": "stt"
|
|
1861
|
+
};
|
|
1862
|
+
var CATALOG_QUERIES = [
|
|
1863
|
+
{ pipeline_tag: "text-to-image", limit: 50 },
|
|
1864
|
+
{ pipeline_tag: "text-to-video", limit: 30 },
|
|
1865
|
+
{ pipeline_tag: "text-to-audio", limit: 30 },
|
|
1866
|
+
{ pipeline_tag: "text-to-speech", limit: 30 },
|
|
1867
|
+
{ pipeline_tag: "automatic-speech-recognition", limit: 30 },
|
|
1868
|
+
{ pipeline_tag: "text-to-image", limit: 100, library: "diffusers" }
|
|
1869
|
+
];
|
|
1870
|
+
async function fetchJsonTimeout(url, timeoutMs = FETCH_TIMEOUT_MS3) {
|
|
1871
|
+
const controller = new AbortController();
|
|
1872
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1873
|
+
try {
|
|
1874
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
1875
|
+
if (!res.ok) return null;
|
|
1876
|
+
return await res.json();
|
|
1877
|
+
} catch {
|
|
1878
|
+
return null;
|
|
1879
|
+
} finally {
|
|
1880
|
+
clearTimeout(timer);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
var HfLocalProvider = class {
|
|
1884
|
+
id = "hf-local";
|
|
1885
|
+
name = "HuggingFace Local Models";
|
|
1886
|
+
modalities = ["image", "video", "tts", "stt"];
|
|
1887
|
+
isLocal = true;
|
|
1888
|
+
cachedModels = null;
|
|
1889
|
+
async ping() {
|
|
1890
|
+
return true;
|
|
1891
|
+
}
|
|
1892
|
+
async listModels(modality) {
|
|
1893
|
+
if (!this.cachedModels) {
|
|
1894
|
+
const [catalog, installed] = await Promise.all([
|
|
1895
|
+
this.fetchCatalog(),
|
|
1896
|
+
this.scanLocalCache()
|
|
1897
|
+
]);
|
|
1898
|
+
const modelMap = /* @__PURE__ */ new Map();
|
|
1899
|
+
for (const m of catalog) modelMap.set(m.id, m);
|
|
1900
|
+
for (const m of installed) modelMap.set(m.id, m);
|
|
1901
|
+
this.cachedModels = Array.from(modelMap.values());
|
|
1902
|
+
}
|
|
1903
|
+
if (modality) return this.cachedModels.filter((m) => m.modality === modality);
|
|
1904
|
+
return this.cachedModels;
|
|
1905
|
+
}
|
|
1906
|
+
async fetchCatalog() {
|
|
1907
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1908
|
+
const models = [];
|
|
1909
|
+
const logo = getProviderLogo("huggingface");
|
|
1910
|
+
const results = await Promise.allSettled(
|
|
1911
|
+
CATALOG_QUERIES.map(async (q) => {
|
|
1912
|
+
const params = new URLSearchParams({
|
|
1913
|
+
pipeline_tag: q.pipeline_tag,
|
|
1914
|
+
sort: "downloads",
|
|
1915
|
+
limit: String(q.limit)
|
|
1916
|
+
});
|
|
1917
|
+
if (q.library) params.set("library", q.library);
|
|
1918
|
+
return fetchJsonTimeout(`${HF_HUB_API2}?${params}`);
|
|
1919
|
+
})
|
|
1920
|
+
);
|
|
1921
|
+
for (const result of results) {
|
|
1922
|
+
if (result.status !== "fulfilled" || !Array.isArray(result.value)) continue;
|
|
1923
|
+
for (const entry of result.value) {
|
|
1924
|
+
const id = entry.id ?? entry.modelId;
|
|
1925
|
+
if (!id || seen.has(id)) continue;
|
|
1926
|
+
seen.add(id);
|
|
1927
|
+
const pipelineTag = entry.pipeline_tag ?? "";
|
|
1928
|
+
const modality = PIPELINE_TAG_TO_MODALITY[pipelineTag] ?? "image";
|
|
1929
|
+
models.push({
|
|
1930
|
+
id,
|
|
1931
|
+
provider: "hf-local",
|
|
1932
|
+
name: id.split("/").pop() ?? id,
|
|
1933
|
+
modality,
|
|
1934
|
+
local: true,
|
|
1935
|
+
cost: { price: 0, unit: "free" },
|
|
1936
|
+
logo,
|
|
1937
|
+
status: "available",
|
|
1938
|
+
localInfo: {
|
|
1939
|
+
sizeBytes: 0,
|
|
1940
|
+
runtime: "huggingface",
|
|
1941
|
+
family: entry.library_name
|
|
1942
|
+
},
|
|
1943
|
+
capabilities: {}
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
return models;
|
|
1948
|
+
}
|
|
1949
|
+
async scanLocalCache() {
|
|
1950
|
+
const models = [];
|
|
1951
|
+
const cacheDir = (0, import_node_path.join)((0, import_node_os.homedir)(), ".cache", "huggingface", "hub");
|
|
1952
|
+
const logo = getProviderLogo("huggingface");
|
|
1953
|
+
try {
|
|
1954
|
+
const entries = await (0, import_promises.readdir)(cacheDir, { withFileTypes: true });
|
|
1955
|
+
for (const entry of entries) {
|
|
1956
|
+
if (!entry.isDirectory() || !entry.name.startsWith("models--")) continue;
|
|
1957
|
+
const parts = entry.name.replace("models--", "").split("--");
|
|
1958
|
+
const modelId = parts.join("/");
|
|
1959
|
+
const modelDir = (0, import_node_path.join)(cacheDir, entry.name);
|
|
1960
|
+
let snapshotHash;
|
|
1961
|
+
try {
|
|
1962
|
+
snapshotHash = (await (0, import_promises.readFile)((0, import_node_path.join)(modelDir, "refs", "main"), "utf-8")).trim();
|
|
1963
|
+
} catch {
|
|
1964
|
+
continue;
|
|
1965
|
+
}
|
|
1966
|
+
let pipelineTag = "";
|
|
1967
|
+
const snapshotDir = (0, import_node_path.join)(modelDir, "snapshots", snapshotHash);
|
|
1968
|
+
try {
|
|
1969
|
+
const modelIndex = JSON.parse(await (0, import_promises.readFile)((0, import_node_path.join)(snapshotDir, "model_index.json"), "utf-8"));
|
|
1970
|
+
if (modelIndex._class_name?.includes("Stable") || modelIndex._class_name?.includes("Flux")) {
|
|
1971
|
+
pipelineTag = "text-to-image";
|
|
1972
|
+
} else if (modelIndex._class_name?.includes("Video") || modelIndex._class_name?.includes("Animate")) {
|
|
1973
|
+
pipelineTag = "text-to-video";
|
|
1974
|
+
}
|
|
1975
|
+
} catch {
|
|
1976
|
+
try {
|
|
1977
|
+
const config = JSON.parse(await (0, import_promises.readFile)((0, import_node_path.join)(snapshotDir, "config.json"), "utf-8"));
|
|
1978
|
+
if (config.task_specific_params?.["text-to-image"]) pipelineTag = "text-to-image";
|
|
1979
|
+
else if (config.model_type?.includes("whisper")) pipelineTag = "automatic-speech-recognition";
|
|
1980
|
+
} catch {
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
const modality = PIPELINE_TAG_TO_MODALITY[pipelineTag] ?? "image";
|
|
1984
|
+
models.push({
|
|
1985
|
+
id: modelId,
|
|
1986
|
+
provider: "hf-local",
|
|
1987
|
+
name: modelId.split("/").pop() ?? modelId,
|
|
1988
|
+
modality,
|
|
1989
|
+
local: true,
|
|
1990
|
+
cost: { price: 0, unit: "free" },
|
|
1991
|
+
logo,
|
|
1992
|
+
status: "installed",
|
|
1993
|
+
localInfo: {
|
|
1994
|
+
sizeBytes: 0,
|
|
1995
|
+
runtime: "huggingface",
|
|
1996
|
+
diskPath: snapshotDir
|
|
1997
|
+
}
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
2000
|
+
} catch {
|
|
2001
|
+
}
|
|
2002
|
+
return models;
|
|
2003
|
+
}
|
|
2004
|
+
};
|
|
2005
|
+
|
|
2006
|
+
// src/providers/whisper-local.ts
|
|
2007
|
+
var import_node_child_process = require("child_process");
|
|
2008
|
+
var import_promises2 = require("fs/promises");
|
|
2009
|
+
var import_node_path2 = require("path");
|
|
2010
|
+
var import_node_os2 = require("os");
|
|
2011
|
+
var WHISPER_MODELS = ["tiny", "base", "small", "medium", "large", "large-v2", "large-v3", "turbo"];
|
|
2012
|
+
function runPython(code, timeoutMs = 5e3) {
|
|
2013
|
+
return new Promise((resolve, reject) => {
|
|
2014
|
+
const proc = (0, import_node_child_process.execFile)("python3", ["-c", code], { timeout: timeoutMs }, (err, stdout) => {
|
|
2015
|
+
if (err) reject(err);
|
|
2016
|
+
else resolve(stdout.trim());
|
|
2017
|
+
});
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
async function fileExists(path) {
|
|
2021
|
+
try {
|
|
2022
|
+
await (0, import_promises2.access)(path);
|
|
2023
|
+
return true;
|
|
2024
|
+
} catch {
|
|
2025
|
+
return false;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
var WhisperLocalProvider = class {
|
|
2029
|
+
id = "whisper-local";
|
|
2030
|
+
name = "Whisper (Local)";
|
|
2031
|
+
modalities = ["stt"];
|
|
2032
|
+
isLocal = true;
|
|
2033
|
+
runtime = null;
|
|
2034
|
+
async ping() {
|
|
2035
|
+
return await this.detectRuntime() !== null;
|
|
2036
|
+
}
|
|
2037
|
+
async detectRuntime() {
|
|
2038
|
+
if (this.runtime) return this.runtime;
|
|
2039
|
+
try {
|
|
2040
|
+
await runPython('import faster_whisper; print("ok")');
|
|
2041
|
+
this.runtime = "faster-whisper";
|
|
2042
|
+
return this.runtime;
|
|
2043
|
+
} catch {
|
|
2044
|
+
}
|
|
2045
|
+
try {
|
|
2046
|
+
await runPython("import whisper; print(whisper.__version__)");
|
|
2047
|
+
this.runtime = "whisper";
|
|
2048
|
+
return this.runtime;
|
|
2049
|
+
} catch {
|
|
2050
|
+
}
|
|
2051
|
+
return null;
|
|
2052
|
+
}
|
|
2053
|
+
async listModels(_modality) {
|
|
2054
|
+
if (_modality && _modality !== "stt") return [];
|
|
2055
|
+
const runtime = await this.detectRuntime();
|
|
2056
|
+
if (!runtime) return [];
|
|
2057
|
+
const logo = getProviderLogo("huggingface");
|
|
2058
|
+
const models = [];
|
|
2059
|
+
for (const name of WHISPER_MODELS) {
|
|
2060
|
+
const installed = await this.isModelCached(name, runtime);
|
|
2061
|
+
models.push({
|
|
2062
|
+
id: `whisper-${name}`,
|
|
2063
|
+
provider: "whisper-local",
|
|
2064
|
+
name: `Whisper ${name}`,
|
|
2065
|
+
modality: "stt",
|
|
2066
|
+
local: true,
|
|
2067
|
+
cost: { price: 0, unit: "free" },
|
|
2068
|
+
logo,
|
|
2069
|
+
status: installed ? "installed" : "available",
|
|
2070
|
+
localInfo: {
|
|
2071
|
+
sizeBytes: 0,
|
|
2072
|
+
runtime
|
|
2073
|
+
}
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
return models;
|
|
2077
|
+
}
|
|
2078
|
+
async isModelCached(name, runtime) {
|
|
2079
|
+
if (runtime === "whisper") {
|
|
2080
|
+
return fileExists((0, import_node_path2.join)((0, import_node_os2.homedir)(), ".cache", "whisper", `${name}.pt`));
|
|
2081
|
+
}
|
|
2082
|
+
const hfDir = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".cache", "huggingface", "hub", `models--Systran--faster-whisper-${name}`);
|
|
2083
|
+
return fileExists(hfDir);
|
|
2084
|
+
}
|
|
2085
|
+
async transcribe(options) {
|
|
2086
|
+
const runtime = await this.detectRuntime();
|
|
2087
|
+
if (!runtime) throw new Error("Whisper is not installed");
|
|
2088
|
+
const model = options.model?.replace("whisper-", "") ?? "base";
|
|
2089
|
+
const lang = options.language ? `--language ${options.language}` : "";
|
|
2090
|
+
const task = options.task ?? "transcribe";
|
|
2091
|
+
if (runtime === "faster-whisper") {
|
|
2092
|
+
const code = `
|
|
2093
|
+
import json, sys
|
|
2094
|
+
from faster_whisper import WhisperModel
|
|
2095
|
+
model = WhisperModel("${model}")
|
|
2096
|
+
segments, info = model.transcribe("${options.audio}", task="${task}"${options.language ? `, language="${options.language}"` : ""})
|
|
2097
|
+
segs = [{"start": s.start, "end": s.end, "text": s.text} for s in segments]
|
|
2098
|
+
print(json.dumps({"text": " ".join(s["text"] for s in segs), "language": info.language, "duration": info.duration, "segments": segs}))
|
|
2099
|
+
`;
|
|
2100
|
+
const output = await runPython(code, 12e4);
|
|
2101
|
+
return JSON.parse(output);
|
|
2102
|
+
} else {
|
|
2103
|
+
const code = `
|
|
2104
|
+
import json, whisper
|
|
2105
|
+
model = whisper.load_model("${model}")
|
|
2106
|
+
result = model.transcribe("${options.audio}", task="${task}"${options.language ? `, language="${options.language}"` : ""})
|
|
2107
|
+
segs = [{"start": s["start"], "end": s["end"], "text": s["text"]} for s in result.get("segments", [])]
|
|
2108
|
+
print(json.dumps({"text": result["text"], "language": result.get("language", ""), "duration": 0, "segments": segs}))
|
|
2109
|
+
`;
|
|
2110
|
+
const output = await runPython(code, 12e4);
|
|
2111
|
+
return JSON.parse(output);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
};
|
|
2115
|
+
|
|
2116
|
+
// src/providers/audiocraft.ts
|
|
2117
|
+
var import_node_child_process2 = require("child_process");
|
|
2118
|
+
var import_promises3 = require("fs/promises");
|
|
2119
|
+
var import_node_path3 = require("path");
|
|
2120
|
+
var import_node_os3 = require("os");
|
|
2121
|
+
var AUDIOCRAFT_MODELS = [
|
|
2122
|
+
{ id: "musicgen-small", name: "MusicGen Small" },
|
|
2123
|
+
{ id: "musicgen-medium", name: "MusicGen Medium" },
|
|
2124
|
+
{ id: "musicgen-large", name: "MusicGen Large" },
|
|
2125
|
+
{ id: "musicgen-melody", name: "MusicGen Melody" },
|
|
2126
|
+
{ id: "audiogen-medium", name: "AudioGen Medium" }
|
|
2127
|
+
];
|
|
2128
|
+
function runPython2(code, timeoutMs = 5e3) {
|
|
2129
|
+
return new Promise((resolve, reject) => {
|
|
2130
|
+
(0, import_node_child_process2.execFile)("python3", ["-c", code], { timeout: timeoutMs }, (err, stdout) => {
|
|
2131
|
+
if (err) reject(err);
|
|
2132
|
+
else resolve(stdout.trim());
|
|
2133
|
+
});
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
async function fileExists2(path) {
|
|
2137
|
+
try {
|
|
2138
|
+
await (0, import_promises3.access)(path);
|
|
2139
|
+
return true;
|
|
2140
|
+
} catch {
|
|
2141
|
+
return false;
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
var AudioCraftProvider = class {
|
|
2145
|
+
id = "audiocraft";
|
|
2146
|
+
name = "AudioCraft (Local)";
|
|
2147
|
+
modalities = ["music"];
|
|
2148
|
+
isLocal = true;
|
|
2149
|
+
detected = null;
|
|
2150
|
+
async ping() {
|
|
2151
|
+
if (this.detected !== null) return this.detected;
|
|
2152
|
+
try {
|
|
2153
|
+
await runPython2('import audiocraft; print("ok")');
|
|
2154
|
+
this.detected = true;
|
|
2155
|
+
} catch {
|
|
2156
|
+
this.detected = false;
|
|
2157
|
+
}
|
|
2158
|
+
return this.detected;
|
|
2159
|
+
}
|
|
2160
|
+
async listModels(_modality) {
|
|
2161
|
+
if (_modality && _modality !== "music") return [];
|
|
2162
|
+
if (!await this.ping()) return [];
|
|
2163
|
+
const logo = getProviderLogo("meta");
|
|
2164
|
+
const models = [];
|
|
2165
|
+
for (const m of AUDIOCRAFT_MODELS) {
|
|
2166
|
+
const hfDir = (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".cache", "huggingface", "hub", `models--facebook--${m.id}`);
|
|
2167
|
+
const installed = await fileExists2(hfDir);
|
|
2168
|
+
models.push({
|
|
2169
|
+
id: m.id,
|
|
2170
|
+
provider: "audiocraft",
|
|
2171
|
+
name: m.name,
|
|
2172
|
+
modality: "music",
|
|
2173
|
+
local: true,
|
|
2174
|
+
cost: { price: 0, unit: "free" },
|
|
2175
|
+
logo,
|
|
2176
|
+
status: installed ? "installed" : "available",
|
|
2177
|
+
localInfo: {
|
|
2178
|
+
sizeBytes: 0,
|
|
2179
|
+
runtime: "audiocraft"
|
|
2180
|
+
}
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
2183
|
+
return models;
|
|
2184
|
+
}
|
|
2185
|
+
};
|
|
2186
|
+
|
|
2187
|
+
// src/providers/openai-compat.ts
|
|
2188
|
+
var FETCH_TIMEOUT_MS4 = 5e3;
|
|
2189
|
+
var KNOWN_LOCAL_SERVERS = [
|
|
2190
|
+
{ port: 8080, name: "llama.cpp / LocalAI", id: "llamacpp" },
|
|
2191
|
+
{ port: 1234, name: "LM Studio", id: "lmstudio" },
|
|
2192
|
+
{ port: 8e3, name: "vLLM", id: "vllm" },
|
|
2193
|
+
{ port: 5e3, name: "TabbyAPI", id: "tabbyapi" },
|
|
2194
|
+
{ port: 5001, name: "KoboldCpp", id: "koboldcpp" },
|
|
2195
|
+
{ port: 1337, name: "Jan", id: "jan" }
|
|
2196
|
+
];
|
|
2197
|
+
async function fetchJsonTimeout2(url, headers, timeoutMs = FETCH_TIMEOUT_MS4) {
|
|
2198
|
+
const controller = new AbortController();
|
|
2199
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2200
|
+
try {
|
|
2201
|
+
const res = await fetch(url, { signal: controller.signal, headers });
|
|
2202
|
+
if (!res.ok) return null;
|
|
2203
|
+
return await res.json();
|
|
2204
|
+
} catch {
|
|
2205
|
+
return null;
|
|
2206
|
+
} finally {
|
|
2207
|
+
clearTimeout(timer);
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
var OpenAICompatProvider = class {
|
|
2211
|
+
id;
|
|
2212
|
+
name;
|
|
2213
|
+
modalities = ["llm"];
|
|
2214
|
+
isLocal = true;
|
|
2215
|
+
baseUrl;
|
|
2216
|
+
headers;
|
|
2217
|
+
constructor(config) {
|
|
2218
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
2219
|
+
this.id = config.id ?? `openai-compat-${new URL(config.baseUrl).port}`;
|
|
2220
|
+
this.name = config.name ?? `OpenAI-Compatible (${this.baseUrl})`;
|
|
2221
|
+
this.headers = { "Content-Type": "application/json" };
|
|
2222
|
+
if (config.apiKey) {
|
|
2223
|
+
this.headers["Authorization"] = `Bearer ${config.apiKey}`;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
async ping() {
|
|
2227
|
+
const data = await fetchJsonTimeout2(`${this.baseUrl}/v1/models`, this.headers, 2e3);
|
|
2228
|
+
return data !== null;
|
|
2229
|
+
}
|
|
2230
|
+
async listModels(_modality) {
|
|
2231
|
+
if (_modality && _modality !== "llm") return [];
|
|
2232
|
+
const data = await fetchJsonTimeout2(`${this.baseUrl}/v1/models`, this.headers);
|
|
2233
|
+
if (!data?.data || !Array.isArray(data.data)) return [];
|
|
2234
|
+
const logo = getProviderLogo("openai");
|
|
2235
|
+
return data.data.map((m) => ({
|
|
2236
|
+
id: m.id,
|
|
2237
|
+
provider: this.id,
|
|
2238
|
+
name: m.id,
|
|
2239
|
+
modality: "llm",
|
|
2240
|
+
local: true,
|
|
2241
|
+
cost: { price: 0, unit: "free" },
|
|
2242
|
+
logo,
|
|
2243
|
+
status: "running",
|
|
2244
|
+
localInfo: {
|
|
2245
|
+
sizeBytes: 0,
|
|
2246
|
+
runtime: this.id
|
|
2247
|
+
},
|
|
2248
|
+
capabilities: {
|
|
2249
|
+
supportsStreaming: true
|
|
2250
|
+
}
|
|
2251
|
+
}));
|
|
2252
|
+
}
|
|
2253
|
+
async chat(options) {
|
|
2254
|
+
const start = Date.now();
|
|
2255
|
+
const model = options.model ?? "default";
|
|
2256
|
+
const body = {
|
|
2257
|
+
model,
|
|
2258
|
+
messages: options.messages,
|
|
2259
|
+
stream: false
|
|
2260
|
+
};
|
|
2261
|
+
if (options.temperature !== void 0) body.temperature = options.temperature;
|
|
2262
|
+
if (options.maxTokens !== void 0) body.max_tokens = options.maxTokens;
|
|
2263
|
+
if (options.jsonMode) body.response_format = { type: "json_object" };
|
|
2264
|
+
const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
|
2265
|
+
method: "POST",
|
|
2266
|
+
headers: this.headers,
|
|
2267
|
+
body: JSON.stringify(body)
|
|
2268
|
+
});
|
|
2269
|
+
if (!res.ok) throw new Error(`OpenAI-compat chat failed: ${res.status} ${await res.text()}`);
|
|
2270
|
+
const data = await res.json();
|
|
2271
|
+
const choice = data.choices?.[0];
|
|
2272
|
+
return {
|
|
2273
|
+
content: choice?.message?.content ?? "",
|
|
2274
|
+
provider: this.id,
|
|
2275
|
+
model,
|
|
2276
|
+
modality: "llm",
|
|
2277
|
+
latencyMs: Date.now() - start,
|
|
2278
|
+
usage: {
|
|
2279
|
+
cost: 0,
|
|
2280
|
+
input: data.usage?.prompt_tokens ?? 0,
|
|
2281
|
+
output: data.usage?.completion_tokens ?? 0,
|
|
2282
|
+
unit: "tokens"
|
|
2283
|
+
}
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
stream(options) {
|
|
2287
|
+
const self = this;
|
|
2288
|
+
const start = Date.now();
|
|
2289
|
+
let aborted = false;
|
|
2290
|
+
let resolveResult = null;
|
|
2291
|
+
let rejectResult = null;
|
|
2292
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
2293
|
+
resolveResult = resolve;
|
|
2294
|
+
rejectResult = reject;
|
|
2295
|
+
});
|
|
2296
|
+
const model = options.model ?? "default";
|
|
2297
|
+
const body = {
|
|
2298
|
+
model,
|
|
2299
|
+
messages: options.messages,
|
|
2300
|
+
stream: true
|
|
2301
|
+
};
|
|
2302
|
+
if (options.temperature !== void 0) body.temperature = options.temperature;
|
|
2303
|
+
if (options.maxTokens !== void 0) body.max_tokens = options.maxTokens;
|
|
2304
|
+
const asyncIterator = {
|
|
2305
|
+
async *[Symbol.asyncIterator]() {
|
|
2306
|
+
try {
|
|
2307
|
+
const res = await fetch(`${self.baseUrl}/v1/chat/completions`, {
|
|
2308
|
+
method: "POST",
|
|
2309
|
+
headers: self.headers,
|
|
2310
|
+
body: JSON.stringify(body)
|
|
2311
|
+
});
|
|
2312
|
+
if (!res.ok) throw new Error(`Stream failed: ${res.status} ${await res.text()}`);
|
|
2313
|
+
const reader = res.body.getReader();
|
|
2314
|
+
const decoder = new TextDecoder();
|
|
2315
|
+
let fullContent = "";
|
|
2316
|
+
let buffer = "";
|
|
2317
|
+
while (!aborted) {
|
|
2318
|
+
const { done, value } = await reader.read();
|
|
2319
|
+
if (done) break;
|
|
2320
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2321
|
+
const lines = buffer.split("\n");
|
|
2322
|
+
buffer = lines.pop() ?? "";
|
|
2323
|
+
for (const line of lines) {
|
|
2324
|
+
if (!line.startsWith("data: ") || line === "data: [DONE]") continue;
|
|
2325
|
+
try {
|
|
2326
|
+
const chunk = JSON.parse(line.slice(6));
|
|
2327
|
+
const delta = chunk.choices?.[0]?.delta?.content;
|
|
2328
|
+
if (delta) {
|
|
2329
|
+
fullContent += delta;
|
|
2330
|
+
yield { type: "text_delta", delta };
|
|
2331
|
+
}
|
|
2332
|
+
} catch {
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
const result = {
|
|
2337
|
+
content: fullContent,
|
|
2338
|
+
provider: self.id,
|
|
2339
|
+
model,
|
|
2340
|
+
modality: "llm",
|
|
2341
|
+
latencyMs: Date.now() - start,
|
|
2342
|
+
usage: { cost: 0, unit: "tokens" }
|
|
2343
|
+
};
|
|
2344
|
+
resolveResult?.(result);
|
|
2345
|
+
yield { type: "done", result };
|
|
2346
|
+
} catch (err) {
|
|
2347
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2348
|
+
rejectResult?.(error);
|
|
2349
|
+
yield { type: "error", error };
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
};
|
|
2353
|
+
return {
|
|
2354
|
+
[Symbol.asyncIterator]: () => asyncIterator[Symbol.asyncIterator](),
|
|
2355
|
+
result: () => resultPromise,
|
|
2356
|
+
abort: () => {
|
|
2357
|
+
aborted = true;
|
|
2358
|
+
}
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
};
|
|
2362
|
+
async function detectOpenAICompatServers() {
|
|
2363
|
+
const providers = [];
|
|
2364
|
+
const results = await Promise.allSettled(
|
|
2365
|
+
KNOWN_LOCAL_SERVERS.map(async (server) => {
|
|
2366
|
+
const baseUrl = `http://localhost:${server.port}`;
|
|
2367
|
+
const provider = new OpenAICompatProvider({
|
|
2368
|
+
baseUrl,
|
|
2369
|
+
name: server.name,
|
|
2370
|
+
id: server.id
|
|
2371
|
+
});
|
|
2372
|
+
const ok = await provider.ping();
|
|
2373
|
+
if (ok) return provider;
|
|
2374
|
+
return null;
|
|
2375
|
+
})
|
|
2376
|
+
);
|
|
2377
|
+
for (const result of results) {
|
|
2378
|
+
if (result.status === "fulfilled" && result.value) {
|
|
2379
|
+
providers.push(result.value);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
return providers;
|
|
2383
|
+
}
|
|
2384
|
+
|
|
1399
2385
|
// src/noosphere.ts
|
|
1400
2386
|
var Noosphere = class {
|
|
1401
2387
|
config;
|
|
@@ -1586,6 +2572,30 @@ var Noosphere = class {
|
|
|
1586
2572
|
getUsage(options) {
|
|
1587
2573
|
return this.tracker.getSummary(options);
|
|
1588
2574
|
}
|
|
2575
|
+
// --- Local Model Management ---
|
|
2576
|
+
async installModel(name) {
|
|
2577
|
+
if (!this.initialized) await this.init();
|
|
2578
|
+
const provider = this.registry.getProvider("ollama");
|
|
2579
|
+
if (!provider) throw new NoosphereError("Ollama provider not available", { code: "PROVIDER_UNAVAILABLE", provider: "ollama", modality: "llm" });
|
|
2580
|
+
return provider.pullModel(name);
|
|
2581
|
+
}
|
|
2582
|
+
async uninstallModel(name) {
|
|
2583
|
+
if (!this.initialized) await this.init();
|
|
2584
|
+
const provider = this.registry.getProvider("ollama");
|
|
2585
|
+
if (!provider) throw new NoosphereError("Ollama provider not available", { code: "PROVIDER_UNAVAILABLE", provider: "ollama", modality: "llm" });
|
|
2586
|
+
await provider.deleteModel(name);
|
|
2587
|
+
}
|
|
2588
|
+
async getHardware() {
|
|
2589
|
+
if (!this.initialized) await this.init();
|
|
2590
|
+
const provider = this.registry.getProvider("ollama");
|
|
2591
|
+
if (!provider) return { ollama: false, runningModels: [] };
|
|
2592
|
+
try {
|
|
2593
|
+
const runningModels = await provider.getRunningModels();
|
|
2594
|
+
return { ollama: true, runningModels };
|
|
2595
|
+
} catch {
|
|
2596
|
+
return { ollama: false, runningModels: [] };
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
1589
2599
|
// --- Lifecycle ---
|
|
1590
2600
|
async dispose() {
|
|
1591
2601
|
for (const provider of this.registry.getAllProviders()) {
|
|
@@ -1636,10 +2646,21 @@ var Noosphere = class {
|
|
|
1636
2646
|
return false;
|
|
1637
2647
|
}
|
|
1638
2648
|
};
|
|
2649
|
+
const ollamaCfg = local["ollama"];
|
|
1639
2650
|
const comfyuiCfg = local["comfyui"];
|
|
1640
2651
|
const piperCfg = local["piper"];
|
|
1641
2652
|
const kokoroCfg = local["kokoro"];
|
|
1642
2653
|
await Promise.allSettled([
|
|
2654
|
+
// Ollama — auto-detect even without explicit config
|
|
2655
|
+
(async () => {
|
|
2656
|
+
const host = ollamaCfg?.host ?? "http://localhost";
|
|
2657
|
+
const port = ollamaCfg?.port ?? 11434;
|
|
2658
|
+
const provider = new OllamaProvider({ host, port });
|
|
2659
|
+
const ok = await provider.ping();
|
|
2660
|
+
if (ok) {
|
|
2661
|
+
this.registry.addProvider(provider);
|
|
2662
|
+
}
|
|
2663
|
+
})(),
|
|
1643
2664
|
// ComfyUI
|
|
1644
2665
|
(async () => {
|
|
1645
2666
|
if (comfyuiCfg?.enabled) {
|
|
@@ -1666,6 +2687,29 @@ var Noosphere = class {
|
|
|
1666
2687
|
this.registry.addProvider(new LocalTTSProvider({ id: "kokoro", name: "Kokoro TTS", host: kokoroCfg.host, port: kokoroCfg.port }));
|
|
1667
2688
|
}
|
|
1668
2689
|
}
|
|
2690
|
+
})(),
|
|
2691
|
+
// HuggingFace local model catalog
|
|
2692
|
+
(async () => {
|
|
2693
|
+
this.registry.addProvider(new HfLocalProvider());
|
|
2694
|
+
})(),
|
|
2695
|
+
// Whisper local STT
|
|
2696
|
+
(async () => {
|
|
2697
|
+
const whisper = new WhisperLocalProvider();
|
|
2698
|
+
const ok = await whisper.ping();
|
|
2699
|
+
if (ok) this.registry.addProvider(whisper);
|
|
2700
|
+
})(),
|
|
2701
|
+
// AudioCraft local music generation
|
|
2702
|
+
(async () => {
|
|
2703
|
+
const audiocraft = new AudioCraftProvider();
|
|
2704
|
+
const ok = await audiocraft.ping();
|
|
2705
|
+
if (ok) this.registry.addProvider(audiocraft);
|
|
2706
|
+
})(),
|
|
2707
|
+
// Auto-detect OpenAI-compatible servers
|
|
2708
|
+
(async () => {
|
|
2709
|
+
const servers = await detectOpenAICompatServers();
|
|
2710
|
+
for (const server of servers) {
|
|
2711
|
+
this.registry.addProvider(server);
|
|
2712
|
+
}
|
|
1669
2713
|
})()
|
|
1670
2714
|
]);
|
|
1671
2715
|
}
|
|
@@ -1716,7 +2760,7 @@ var Noosphere = class {
|
|
|
1716
2760
|
break;
|
|
1717
2761
|
}
|
|
1718
2762
|
const delay = backoffMs * Math.pow(2, attempt);
|
|
1719
|
-
await new Promise((
|
|
2763
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1720
2764
|
}
|
|
1721
2765
|
}
|
|
1722
2766
|
throw lastError ?? new NoosphereError("Generation failed", {
|
|
@@ -1758,10 +2802,16 @@ var Noosphere = class {
|
|
|
1758
2802
|
};
|
|
1759
2803
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1760
2804
|
0 && (module.exports = {
|
|
2805
|
+
AudioCraftProvider,
|
|
2806
|
+
HfLocalProvider,
|
|
1761
2807
|
Noosphere,
|
|
1762
2808
|
NoosphereError,
|
|
2809
|
+
OllamaProvider,
|
|
2810
|
+
OpenAICompatProvider,
|
|
1763
2811
|
PROVIDER_IDS,
|
|
1764
2812
|
PROVIDER_LOGOS,
|
|
2813
|
+
WhisperLocalProvider,
|
|
2814
|
+
detectOpenAICompatServers,
|
|
1765
2815
|
getAllProviderLogos,
|
|
1766
2816
|
getProviderLogo
|
|
1767
2817
|
});
|