noosphere 0.5.0 → 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/dist/index.js CHANGED
@@ -394,7 +394,7 @@ var UsageTracker = class {
394
394
  filtered = filtered.filter((e) => e.modality === options.modality);
395
395
  }
396
396
  const byProvider = {};
397
- const byModality = { llm: 0, image: 0, video: 0, tts: 0 };
397
+ const byModality = { llm: 0, image: 0, video: 0, tts: 0, stt: 0, music: 0, embedding: 0 };
398
398
  let totalCost = 0;
399
399
  for (const event of filtered) {
400
400
  totalCost += event.cost;
@@ -1019,11 +1019,55 @@ var ComfyUIProvider = class {
1019
1019
  }
1020
1020
  }
1021
1021
  async listModels(modality) {
1022
+ const models = [];
1023
+ const logo = getProviderLogo("comfyui");
1022
1024
  try {
1023
- const res = await fetch(`${this.baseUrl}/object_info`);
1024
- if (!res.ok) return [];
1025
- const models = [];
1026
- const logo = getProviderLogo("comfyui");
1025
+ const controller = new AbortController();
1026
+ const timer = setTimeout(() => controller.abort(), 5e3);
1027
+ try {
1028
+ const res = await fetch(`${this.baseUrl}/object_info`, { signal: controller.signal });
1029
+ if (res.ok) {
1030
+ const objectInfo = await res.json();
1031
+ const ckptNode = objectInfo?.["CheckpointLoaderSimple"];
1032
+ const ckptNames = ckptNode?.input?.required?.ckpt_name?.[0] ?? [];
1033
+ for (const name of ckptNames) {
1034
+ if (modality && modality !== "image") continue;
1035
+ models.push({
1036
+ id: `comfyui-ckpt-${name}`,
1037
+ provider: "comfyui",
1038
+ name: `ComfyUI: ${name}`,
1039
+ modality: "image",
1040
+ local: true,
1041
+ cost: { price: 0, unit: "free" },
1042
+ logo,
1043
+ status: "installed",
1044
+ localInfo: { sizeBytes: 0, runtime: "comfyui" },
1045
+ capabilities: { maxWidth: 2048, maxHeight: 2048, supportsNegativePrompt: true }
1046
+ });
1047
+ }
1048
+ const loraNode = objectInfo?.["LoraLoader"];
1049
+ const loraNames = loraNode?.input?.required?.lora_name?.[0] ?? [];
1050
+ for (const name of loraNames) {
1051
+ if (modality && modality !== "image") continue;
1052
+ models.push({
1053
+ id: `comfyui-lora-${name}`,
1054
+ provider: "comfyui",
1055
+ name: `LoRA: ${name}`,
1056
+ modality: "image",
1057
+ local: true,
1058
+ cost: { price: 0, unit: "free" },
1059
+ logo,
1060
+ status: "installed",
1061
+ localInfo: { sizeBytes: 0, runtime: "comfyui" }
1062
+ });
1063
+ }
1064
+ }
1065
+ } finally {
1066
+ clearTimeout(timer);
1067
+ }
1068
+ } catch {
1069
+ }
1070
+ if (models.length === 0) {
1027
1071
  if (!modality || modality === "image") {
1028
1072
  models.push({
1029
1073
  id: "comfyui-txt2img",
@@ -1048,10 +1092,43 @@ var ComfyUIProvider = class {
1048
1092
  capabilities: { maxDuration: 10, supportsImageToVideo: true }
1049
1093
  });
1050
1094
  }
1051
- return models;
1052
- } catch {
1053
- return [];
1054
1095
  }
1096
+ if (!modality || modality === "image") {
1097
+ try {
1098
+ const controller = new AbortController();
1099
+ const timer = setTimeout(() => controller.abort(), 5e3);
1100
+ try {
1101
+ const res = await fetch(
1102
+ "https://civitai.com/api/v1/models?types=Checkpoint&sort=Highest%20Rated&limit=50&nsfw=false",
1103
+ { signal: controller.signal }
1104
+ );
1105
+ if (res.ok) {
1106
+ const data = await res.json();
1107
+ for (const item of data.items ?? []) {
1108
+ const version = item.modelVersions?.[0];
1109
+ models.push({
1110
+ id: `civitai-${item.id}`,
1111
+ provider: "comfyui",
1112
+ name: item.name ?? `CivitAI Model ${item.id}`,
1113
+ modality: "image",
1114
+ local: true,
1115
+ cost: { price: 0, unit: "free" },
1116
+ logo,
1117
+ status: "available",
1118
+ localInfo: {
1119
+ sizeBytes: version?.files?.[0]?.sizeKB ? version.files[0].sizeKB * 1024 : 0,
1120
+ runtime: "comfyui"
1121
+ }
1122
+ });
1123
+ }
1124
+ }
1125
+ } finally {
1126
+ clearTimeout(timer);
1127
+ }
1128
+ } catch {
1129
+ }
1130
+ }
1131
+ return models;
1055
1132
  }
1056
1133
  async image(options) {
1057
1134
  const start = Date.now();
@@ -1391,6 +1468,890 @@ var HuggingFaceProvider = class {
1391
1468
  }
1392
1469
  };
1393
1470
 
1471
+ // src/providers/ollama.ts
1472
+ var OLLAMA_FAMILY_TO_PROVIDER = {
1473
+ "llama": "meta",
1474
+ "codellama": "meta",
1475
+ "gemma": "google",
1476
+ "gemma2": "google",
1477
+ "gemma3": "google",
1478
+ "qwen": "qwen",
1479
+ "qwen2": "qwen",
1480
+ "qwen2.5": "qwen",
1481
+ "qwen3": "qwen",
1482
+ "deepseek": "deepseek",
1483
+ "deepcoder": "deepseek",
1484
+ "deepscaler": "deepseek",
1485
+ "qwq": "qwen",
1486
+ "phi": "microsoft",
1487
+ "phi3": "microsoft",
1488
+ "phi4": "microsoft",
1489
+ "mistral": "mistral",
1490
+ "mixtral": "mistral",
1491
+ "codestral": "mistral",
1492
+ "ministral": "mistral",
1493
+ "nemotron": "nvidia",
1494
+ "command": "cohere",
1495
+ "command-r": "cohere",
1496
+ "gpt-oss": "openai",
1497
+ "starcoder": "huggingface",
1498
+ "falcon": "meta",
1499
+ "glm": "zai",
1500
+ "granite": "ibm",
1501
+ "olmo": "meta",
1502
+ "yi": "zai",
1503
+ "minimax": "minimax",
1504
+ "kimi": "meta",
1505
+ "dolphin": "ollama",
1506
+ "wizard": "ollama",
1507
+ "nomic": "ollama",
1508
+ "mxbai": "ollama",
1509
+ "bge": "ollama",
1510
+ "all-minilm": "ollama",
1511
+ "moondream": "ollama"
1512
+ };
1513
+ var VISION_MODELS = /* @__PURE__ */ new Set([
1514
+ "llava",
1515
+ "moondream",
1516
+ "minicpm-v",
1517
+ "llama3.2-vision",
1518
+ "qwen2.5vl",
1519
+ "gemma3",
1520
+ "llava-llama3",
1521
+ "llava-phi3",
1522
+ "bakllava"
1523
+ ]);
1524
+ function inferLogoProvider(modelName, _family) {
1525
+ const base = modelName.split(":")[0].toLowerCase().replace(/^[^/]+\//, "");
1526
+ const sortedPrefixes = Object.entries(OLLAMA_FAMILY_TO_PROVIDER).sort((a, b) => b[0].length - a[0].length);
1527
+ for (const [prefix, provider] of sortedPrefixes) {
1528
+ if (base === prefix || base.startsWith(prefix)) return provider;
1529
+ }
1530
+ return "ollama";
1531
+ }
1532
+ function supportsVision(modelName) {
1533
+ const base = modelName.split(":")[0].toLowerCase();
1534
+ for (const v of VISION_MODELS) {
1535
+ if (base === v || base.startsWith(v)) return true;
1536
+ }
1537
+ return false;
1538
+ }
1539
+ async function fetchJson(url, options) {
1540
+ const timeoutMs = options?.timeoutMs ?? 5e3;
1541
+ const controller = new AbortController();
1542
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1543
+ try {
1544
+ const res = await fetch(url, { ...options, signal: controller.signal });
1545
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1546
+ return await res.json();
1547
+ } finally {
1548
+ clearTimeout(timer);
1549
+ }
1550
+ }
1551
+ var OllamaProvider = class {
1552
+ id = "ollama";
1553
+ name = "Ollama (Local)";
1554
+ modalities = ["llm"];
1555
+ isLocal = true;
1556
+ baseUrl;
1557
+ constructor(config) {
1558
+ const host = config?.host ?? "http://localhost";
1559
+ const port = config?.port ?? 11434;
1560
+ const cleanHost = host.replace(/\/+$/, "");
1561
+ const hasPort = /:\d+$/.test(cleanHost);
1562
+ this.baseUrl = hasPort ? cleanHost : `${cleanHost}:${port}`;
1563
+ }
1564
+ async ping() {
1565
+ try {
1566
+ await fetchJson(`${this.baseUrl}/api/version`, { timeoutMs: 2e3 });
1567
+ return true;
1568
+ } catch {
1569
+ return false;
1570
+ }
1571
+ }
1572
+ async listModels(_modality) {
1573
+ if (_modality && _modality !== "llm") return [];
1574
+ const [localData, catalogData, runningData] = await Promise.all([
1575
+ fetchJson(`${this.baseUrl}/api/tags`, { timeoutMs: 5e3 }).catch(() => null),
1576
+ fetchJson("https://ollama.com/api/tags", { timeoutMs: 5e3 }).catch(() => null),
1577
+ fetchJson(`${this.baseUrl}/api/ps`, { timeoutMs: 5e3 }).catch(() => null)
1578
+ ]);
1579
+ const runningNames = /* @__PURE__ */ new Set();
1580
+ if (runningData?.models) {
1581
+ for (const m of runningData.models) {
1582
+ runningNames.add(m.name);
1583
+ runningNames.add(m.model);
1584
+ }
1585
+ }
1586
+ const models = /* @__PURE__ */ new Map();
1587
+ if (localData?.models) {
1588
+ for (const m of localData.models) {
1589
+ const isRunning = runningNames.has(m.name) || runningNames.has(m.model);
1590
+ models.set(m.name, this.toModelInfo(m, isRunning ? "running" : "installed", true));
1591
+ }
1592
+ }
1593
+ if (catalogData?.models) {
1594
+ for (const m of catalogData.models) {
1595
+ const name = m.name;
1596
+ if (!models.has(name)) {
1597
+ models.set(name, this.toModelInfo(m, "available", false));
1598
+ }
1599
+ }
1600
+ }
1601
+ return Array.from(models.values());
1602
+ }
1603
+ toModelInfo(m, status, isLocal) {
1604
+ const name = m.name ?? m.model ?? "unknown";
1605
+ const family = m.details?.family;
1606
+ const logoProvider = inferLogoProvider(name, family);
1607
+ return {
1608
+ id: name,
1609
+ provider: "ollama",
1610
+ name,
1611
+ modality: "llm",
1612
+ local: true,
1613
+ cost: { price: 0, unit: "free" },
1614
+ logo: getProviderLogo(logoProvider),
1615
+ status,
1616
+ localInfo: {
1617
+ sizeBytes: m.size ?? 0,
1618
+ family: family ?? m.details?.family,
1619
+ parameterSize: m.details?.parameter_size,
1620
+ quantization: m.details?.quantization_level,
1621
+ format: m.details?.format,
1622
+ digest: m.digest,
1623
+ modifiedAt: m.modified_at,
1624
+ running: status === "running",
1625
+ runtime: "ollama"
1626
+ },
1627
+ capabilities: {
1628
+ contextWindow: 128e3,
1629
+ supportsVision: supportsVision(name),
1630
+ supportsStreaming: true
1631
+ }
1632
+ };
1633
+ }
1634
+ async chat(options) {
1635
+ const start = Date.now();
1636
+ const messages = options.messages.map((m) => ({
1637
+ role: m.role,
1638
+ content: m.content
1639
+ }));
1640
+ const body = {
1641
+ model: options.model ?? "llama3.2",
1642
+ messages,
1643
+ stream: false
1644
+ };
1645
+ if (options.temperature !== void 0 || options.maxTokens !== void 0) {
1646
+ body.options = {};
1647
+ if (options.temperature !== void 0) body.options.temperature = options.temperature;
1648
+ if (options.maxTokens !== void 0) body.options.num_predict = options.maxTokens;
1649
+ }
1650
+ const res = await fetch(`${this.baseUrl}/api/chat`, {
1651
+ method: "POST",
1652
+ headers: { "Content-Type": "application/json" },
1653
+ body: JSON.stringify(body)
1654
+ });
1655
+ if (!res.ok) {
1656
+ throw new Error(`Ollama chat failed: ${res.status} ${await res.text()}`);
1657
+ }
1658
+ const data = await res.json();
1659
+ return {
1660
+ content: data.message?.content ?? "",
1661
+ provider: "ollama",
1662
+ model: options.model ?? "llama3.2",
1663
+ modality: "llm",
1664
+ latencyMs: Date.now() - start,
1665
+ usage: {
1666
+ cost: 0,
1667
+ input: data.prompt_eval_count ?? 0,
1668
+ output: data.eval_count ?? 0,
1669
+ unit: "tokens"
1670
+ }
1671
+ };
1672
+ }
1673
+ stream(options) {
1674
+ const self = this;
1675
+ const start = Date.now();
1676
+ let aborted = false;
1677
+ let resolveResult = null;
1678
+ let rejectResult = null;
1679
+ const resultPromise = new Promise((resolve, reject) => {
1680
+ resolveResult = resolve;
1681
+ rejectResult = reject;
1682
+ });
1683
+ const messages = options.messages.map((m) => ({
1684
+ role: m.role,
1685
+ content: m.content
1686
+ }));
1687
+ const body = {
1688
+ model: options.model ?? "llama3.2",
1689
+ messages,
1690
+ stream: true
1691
+ };
1692
+ if (options.temperature !== void 0 || options.maxTokens !== void 0) {
1693
+ body.options = {};
1694
+ if (options.temperature !== void 0) body.options.temperature = options.temperature;
1695
+ if (options.maxTokens !== void 0) body.options.num_predict = options.maxTokens;
1696
+ }
1697
+ const asyncIterator = {
1698
+ async *[Symbol.asyncIterator]() {
1699
+ try {
1700
+ const res = await fetch(`${self.baseUrl}/api/chat`, {
1701
+ method: "POST",
1702
+ headers: { "Content-Type": "application/json" },
1703
+ body: JSON.stringify(body)
1704
+ });
1705
+ if (!res.ok) {
1706
+ throw new Error(`Ollama stream failed: ${res.status} ${await res.text()}`);
1707
+ }
1708
+ const reader = res.body.getReader();
1709
+ const decoder = new TextDecoder();
1710
+ let fullContent = "";
1711
+ let finalData = null;
1712
+ let buffer = "";
1713
+ while (!aborted) {
1714
+ const { done, value } = await reader.read();
1715
+ if (done) break;
1716
+ buffer += decoder.decode(value, { stream: true });
1717
+ const lines = buffer.split("\n");
1718
+ buffer = lines.pop() ?? "";
1719
+ for (const line of lines) {
1720
+ if (!line.trim()) continue;
1721
+ try {
1722
+ const chunk = JSON.parse(line);
1723
+ if (chunk.message?.content) {
1724
+ fullContent += chunk.message.content;
1725
+ yield { type: "text_delta", delta: chunk.message.content };
1726
+ }
1727
+ if (chunk.done) {
1728
+ finalData = chunk;
1729
+ }
1730
+ } catch {
1731
+ }
1732
+ }
1733
+ }
1734
+ const result = {
1735
+ content: fullContent,
1736
+ provider: "ollama",
1737
+ model: options.model ?? "llama3.2",
1738
+ modality: "llm",
1739
+ latencyMs: Date.now() - start,
1740
+ usage: {
1741
+ cost: 0,
1742
+ input: finalData?.prompt_eval_count ?? 0,
1743
+ output: finalData?.eval_count ?? 0,
1744
+ unit: "tokens"
1745
+ }
1746
+ };
1747
+ resolveResult?.(result);
1748
+ yield { type: "done", result };
1749
+ } catch (err) {
1750
+ const error = err instanceof Error ? err : new Error(String(err));
1751
+ rejectResult?.(error);
1752
+ yield { type: "error", error };
1753
+ }
1754
+ }
1755
+ };
1756
+ return {
1757
+ [Symbol.asyncIterator]: () => asyncIterator[Symbol.asyncIterator](),
1758
+ result: () => resultPromise,
1759
+ abort: () => {
1760
+ aborted = true;
1761
+ }
1762
+ };
1763
+ }
1764
+ // --- Extra model management methods ---
1765
+ async *pullModel(name) {
1766
+ const res = await fetch(`${this.baseUrl}/api/pull`, {
1767
+ method: "POST",
1768
+ headers: { "Content-Type": "application/json" },
1769
+ body: JSON.stringify({ name, stream: true })
1770
+ });
1771
+ if (!res.ok) {
1772
+ throw new Error(`Ollama pull failed: ${res.status} ${await res.text()}`);
1773
+ }
1774
+ const reader = res.body.getReader();
1775
+ const decoder = new TextDecoder();
1776
+ let buffer = "";
1777
+ while (true) {
1778
+ const { done, value } = await reader.read();
1779
+ if (done) break;
1780
+ buffer += decoder.decode(value, { stream: true });
1781
+ const lines = buffer.split("\n");
1782
+ buffer = lines.pop() ?? "";
1783
+ for (const line of lines) {
1784
+ if (!line.trim()) continue;
1785
+ try {
1786
+ yield JSON.parse(line);
1787
+ } catch {
1788
+ }
1789
+ }
1790
+ }
1791
+ }
1792
+ async deleteModel(name) {
1793
+ const res = await fetch(`${this.baseUrl}/api/delete`, {
1794
+ method: "DELETE",
1795
+ headers: { "Content-Type": "application/json" },
1796
+ body: JSON.stringify({ name })
1797
+ });
1798
+ if (!res.ok) {
1799
+ throw new Error(`Ollama delete failed: ${res.status} ${await res.text()}`);
1800
+ }
1801
+ }
1802
+ async showModel(name) {
1803
+ const res = await fetch(`${this.baseUrl}/api/show`, {
1804
+ method: "POST",
1805
+ headers: { "Content-Type": "application/json" },
1806
+ body: JSON.stringify({ name })
1807
+ });
1808
+ if (!res.ok) {
1809
+ throw new Error(`Ollama show failed: ${res.status} ${await res.text()}`);
1810
+ }
1811
+ return await res.json();
1812
+ }
1813
+ async getRunningModels() {
1814
+ const data = await fetchJson(`${this.baseUrl}/api/ps`, { timeoutMs: 5e3 });
1815
+ return data?.models ?? [];
1816
+ }
1817
+ };
1818
+
1819
+ // src/providers/hf-local.ts
1820
+ import { readdir, readFile } from "fs/promises";
1821
+ import { join } from "path";
1822
+ import { homedir } from "os";
1823
+ var FETCH_TIMEOUT_MS3 = 5e3;
1824
+ var HF_HUB_API2 = "https://huggingface.co/api/models";
1825
+ var PIPELINE_TAG_TO_MODALITY = {
1826
+ "text-to-image": "image",
1827
+ "text-to-video": "video",
1828
+ "text-to-audio": "tts",
1829
+ "text-to-speech": "tts",
1830
+ "automatic-speech-recognition": "stt"
1831
+ };
1832
+ var CATALOG_QUERIES = [
1833
+ { pipeline_tag: "text-to-image", limit: 50 },
1834
+ { pipeline_tag: "text-to-video", limit: 30 },
1835
+ { pipeline_tag: "text-to-audio", limit: 30 },
1836
+ { pipeline_tag: "text-to-speech", limit: 30 },
1837
+ { pipeline_tag: "automatic-speech-recognition", limit: 30 },
1838
+ { pipeline_tag: "text-to-image", limit: 100, library: "diffusers" }
1839
+ ];
1840
+ async function fetchJsonTimeout(url, timeoutMs = FETCH_TIMEOUT_MS3) {
1841
+ const controller = new AbortController();
1842
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1843
+ try {
1844
+ const res = await fetch(url, { signal: controller.signal });
1845
+ if (!res.ok) return null;
1846
+ return await res.json();
1847
+ } catch {
1848
+ return null;
1849
+ } finally {
1850
+ clearTimeout(timer);
1851
+ }
1852
+ }
1853
+ var HfLocalProvider = class {
1854
+ id = "hf-local";
1855
+ name = "HuggingFace Local Models";
1856
+ modalities = ["image", "video", "tts", "stt"];
1857
+ isLocal = true;
1858
+ cachedModels = null;
1859
+ async ping() {
1860
+ return true;
1861
+ }
1862
+ async listModels(modality) {
1863
+ if (!this.cachedModels) {
1864
+ const [catalog, installed] = await Promise.all([
1865
+ this.fetchCatalog(),
1866
+ this.scanLocalCache()
1867
+ ]);
1868
+ const modelMap = /* @__PURE__ */ new Map();
1869
+ for (const m of catalog) modelMap.set(m.id, m);
1870
+ for (const m of installed) modelMap.set(m.id, m);
1871
+ this.cachedModels = Array.from(modelMap.values());
1872
+ }
1873
+ if (modality) return this.cachedModels.filter((m) => m.modality === modality);
1874
+ return this.cachedModels;
1875
+ }
1876
+ async fetchCatalog() {
1877
+ const seen = /* @__PURE__ */ new Set();
1878
+ const models = [];
1879
+ const logo = getProviderLogo("huggingface");
1880
+ const results = await Promise.allSettled(
1881
+ CATALOG_QUERIES.map(async (q) => {
1882
+ const params = new URLSearchParams({
1883
+ pipeline_tag: q.pipeline_tag,
1884
+ sort: "downloads",
1885
+ limit: String(q.limit)
1886
+ });
1887
+ if (q.library) params.set("library", q.library);
1888
+ return fetchJsonTimeout(`${HF_HUB_API2}?${params}`);
1889
+ })
1890
+ );
1891
+ for (const result of results) {
1892
+ if (result.status !== "fulfilled" || !Array.isArray(result.value)) continue;
1893
+ for (const entry of result.value) {
1894
+ const id = entry.id ?? entry.modelId;
1895
+ if (!id || seen.has(id)) continue;
1896
+ seen.add(id);
1897
+ const pipelineTag = entry.pipeline_tag ?? "";
1898
+ const modality = PIPELINE_TAG_TO_MODALITY[pipelineTag] ?? "image";
1899
+ models.push({
1900
+ id,
1901
+ provider: "hf-local",
1902
+ name: id.split("/").pop() ?? id,
1903
+ modality,
1904
+ local: true,
1905
+ cost: { price: 0, unit: "free" },
1906
+ logo,
1907
+ status: "available",
1908
+ localInfo: {
1909
+ sizeBytes: 0,
1910
+ runtime: "huggingface",
1911
+ family: entry.library_name
1912
+ },
1913
+ capabilities: {}
1914
+ });
1915
+ }
1916
+ }
1917
+ return models;
1918
+ }
1919
+ async scanLocalCache() {
1920
+ const models = [];
1921
+ const cacheDir = join(homedir(), ".cache", "huggingface", "hub");
1922
+ const logo = getProviderLogo("huggingface");
1923
+ try {
1924
+ const entries = await readdir(cacheDir, { withFileTypes: true });
1925
+ for (const entry of entries) {
1926
+ if (!entry.isDirectory() || !entry.name.startsWith("models--")) continue;
1927
+ const parts = entry.name.replace("models--", "").split("--");
1928
+ const modelId = parts.join("/");
1929
+ const modelDir = join(cacheDir, entry.name);
1930
+ let snapshotHash;
1931
+ try {
1932
+ snapshotHash = (await readFile(join(modelDir, "refs", "main"), "utf-8")).trim();
1933
+ } catch {
1934
+ continue;
1935
+ }
1936
+ let pipelineTag = "";
1937
+ const snapshotDir = join(modelDir, "snapshots", snapshotHash);
1938
+ try {
1939
+ const modelIndex = JSON.parse(await readFile(join(snapshotDir, "model_index.json"), "utf-8"));
1940
+ if (modelIndex._class_name?.includes("Stable") || modelIndex._class_name?.includes("Flux")) {
1941
+ pipelineTag = "text-to-image";
1942
+ } else if (modelIndex._class_name?.includes("Video") || modelIndex._class_name?.includes("Animate")) {
1943
+ pipelineTag = "text-to-video";
1944
+ }
1945
+ } catch {
1946
+ try {
1947
+ const config = JSON.parse(await readFile(join(snapshotDir, "config.json"), "utf-8"));
1948
+ if (config.task_specific_params?.["text-to-image"]) pipelineTag = "text-to-image";
1949
+ else if (config.model_type?.includes("whisper")) pipelineTag = "automatic-speech-recognition";
1950
+ } catch {
1951
+ }
1952
+ }
1953
+ const modality = PIPELINE_TAG_TO_MODALITY[pipelineTag] ?? "image";
1954
+ models.push({
1955
+ id: modelId,
1956
+ provider: "hf-local",
1957
+ name: modelId.split("/").pop() ?? modelId,
1958
+ modality,
1959
+ local: true,
1960
+ cost: { price: 0, unit: "free" },
1961
+ logo,
1962
+ status: "installed",
1963
+ localInfo: {
1964
+ sizeBytes: 0,
1965
+ runtime: "huggingface",
1966
+ diskPath: snapshotDir
1967
+ }
1968
+ });
1969
+ }
1970
+ } catch {
1971
+ }
1972
+ return models;
1973
+ }
1974
+ };
1975
+
1976
+ // src/providers/whisper-local.ts
1977
+ import { execFile } from "child_process";
1978
+ import { access } from "fs/promises";
1979
+ import { join as join2 } from "path";
1980
+ import { homedir as homedir2 } from "os";
1981
+ var WHISPER_MODELS = ["tiny", "base", "small", "medium", "large", "large-v2", "large-v3", "turbo"];
1982
+ function runPython(code, timeoutMs = 5e3) {
1983
+ return new Promise((resolve, reject) => {
1984
+ const proc = execFile("python3", ["-c", code], { timeout: timeoutMs }, (err, stdout) => {
1985
+ if (err) reject(err);
1986
+ else resolve(stdout.trim());
1987
+ });
1988
+ });
1989
+ }
1990
+ async function fileExists(path) {
1991
+ try {
1992
+ await access(path);
1993
+ return true;
1994
+ } catch {
1995
+ return false;
1996
+ }
1997
+ }
1998
+ var WhisperLocalProvider = class {
1999
+ id = "whisper-local";
2000
+ name = "Whisper (Local)";
2001
+ modalities = ["stt"];
2002
+ isLocal = true;
2003
+ runtime = null;
2004
+ async ping() {
2005
+ return await this.detectRuntime() !== null;
2006
+ }
2007
+ async detectRuntime() {
2008
+ if (this.runtime) return this.runtime;
2009
+ try {
2010
+ await runPython('import faster_whisper; print("ok")');
2011
+ this.runtime = "faster-whisper";
2012
+ return this.runtime;
2013
+ } catch {
2014
+ }
2015
+ try {
2016
+ await runPython("import whisper; print(whisper.__version__)");
2017
+ this.runtime = "whisper";
2018
+ return this.runtime;
2019
+ } catch {
2020
+ }
2021
+ return null;
2022
+ }
2023
+ async listModels(_modality) {
2024
+ if (_modality && _modality !== "stt") return [];
2025
+ const runtime = await this.detectRuntime();
2026
+ if (!runtime) return [];
2027
+ const logo = getProviderLogo("huggingface");
2028
+ const models = [];
2029
+ for (const name of WHISPER_MODELS) {
2030
+ const installed = await this.isModelCached(name, runtime);
2031
+ models.push({
2032
+ id: `whisper-${name}`,
2033
+ provider: "whisper-local",
2034
+ name: `Whisper ${name}`,
2035
+ modality: "stt",
2036
+ local: true,
2037
+ cost: { price: 0, unit: "free" },
2038
+ logo,
2039
+ status: installed ? "installed" : "available",
2040
+ localInfo: {
2041
+ sizeBytes: 0,
2042
+ runtime
2043
+ }
2044
+ });
2045
+ }
2046
+ return models;
2047
+ }
2048
+ async isModelCached(name, runtime) {
2049
+ if (runtime === "whisper") {
2050
+ return fileExists(join2(homedir2(), ".cache", "whisper", `${name}.pt`));
2051
+ }
2052
+ const hfDir = join2(homedir2(), ".cache", "huggingface", "hub", `models--Systran--faster-whisper-${name}`);
2053
+ return fileExists(hfDir);
2054
+ }
2055
+ async transcribe(options) {
2056
+ const runtime = await this.detectRuntime();
2057
+ if (!runtime) throw new Error("Whisper is not installed");
2058
+ const model = options.model?.replace("whisper-", "") ?? "base";
2059
+ const lang = options.language ? `--language ${options.language}` : "";
2060
+ const task = options.task ?? "transcribe";
2061
+ if (runtime === "faster-whisper") {
2062
+ const code = `
2063
+ import json, sys
2064
+ from faster_whisper import WhisperModel
2065
+ model = WhisperModel("${model}")
2066
+ segments, info = model.transcribe("${options.audio}", task="${task}"${options.language ? `, language="${options.language}"` : ""})
2067
+ segs = [{"start": s.start, "end": s.end, "text": s.text} for s in segments]
2068
+ print(json.dumps({"text": " ".join(s["text"] for s in segs), "language": info.language, "duration": info.duration, "segments": segs}))
2069
+ `;
2070
+ const output = await runPython(code, 12e4);
2071
+ return JSON.parse(output);
2072
+ } else {
2073
+ const code = `
2074
+ import json, whisper
2075
+ model = whisper.load_model("${model}")
2076
+ result = model.transcribe("${options.audio}", task="${task}"${options.language ? `, language="${options.language}"` : ""})
2077
+ segs = [{"start": s["start"], "end": s["end"], "text": s["text"]} for s in result.get("segments", [])]
2078
+ print(json.dumps({"text": result["text"], "language": result.get("language", ""), "duration": 0, "segments": segs}))
2079
+ `;
2080
+ const output = await runPython(code, 12e4);
2081
+ return JSON.parse(output);
2082
+ }
2083
+ }
2084
+ };
2085
+
2086
+ // src/providers/audiocraft.ts
2087
+ import { execFile as execFile2 } from "child_process";
2088
+ import { access as access2 } from "fs/promises";
2089
+ import { join as join3 } from "path";
2090
+ import { homedir as homedir3 } from "os";
2091
+ var AUDIOCRAFT_MODELS = [
2092
+ { id: "musicgen-small", name: "MusicGen Small" },
2093
+ { id: "musicgen-medium", name: "MusicGen Medium" },
2094
+ { id: "musicgen-large", name: "MusicGen Large" },
2095
+ { id: "musicgen-melody", name: "MusicGen Melody" },
2096
+ { id: "audiogen-medium", name: "AudioGen Medium" }
2097
+ ];
2098
+ function runPython2(code, timeoutMs = 5e3) {
2099
+ return new Promise((resolve, reject) => {
2100
+ execFile2("python3", ["-c", code], { timeout: timeoutMs }, (err, stdout) => {
2101
+ if (err) reject(err);
2102
+ else resolve(stdout.trim());
2103
+ });
2104
+ });
2105
+ }
2106
+ async function fileExists2(path) {
2107
+ try {
2108
+ await access2(path);
2109
+ return true;
2110
+ } catch {
2111
+ return false;
2112
+ }
2113
+ }
2114
+ var AudioCraftProvider = class {
2115
+ id = "audiocraft";
2116
+ name = "AudioCraft (Local)";
2117
+ modalities = ["music"];
2118
+ isLocal = true;
2119
+ detected = null;
2120
+ async ping() {
2121
+ if (this.detected !== null) return this.detected;
2122
+ try {
2123
+ await runPython2('import audiocraft; print("ok")');
2124
+ this.detected = true;
2125
+ } catch {
2126
+ this.detected = false;
2127
+ }
2128
+ return this.detected;
2129
+ }
2130
+ async listModels(_modality) {
2131
+ if (_modality && _modality !== "music") return [];
2132
+ if (!await this.ping()) return [];
2133
+ const logo = getProviderLogo("meta");
2134
+ const models = [];
2135
+ for (const m of AUDIOCRAFT_MODELS) {
2136
+ const hfDir = join3(homedir3(), ".cache", "huggingface", "hub", `models--facebook--${m.id}`);
2137
+ const installed = await fileExists2(hfDir);
2138
+ models.push({
2139
+ id: m.id,
2140
+ provider: "audiocraft",
2141
+ name: m.name,
2142
+ modality: "music",
2143
+ local: true,
2144
+ cost: { price: 0, unit: "free" },
2145
+ logo,
2146
+ status: installed ? "installed" : "available",
2147
+ localInfo: {
2148
+ sizeBytes: 0,
2149
+ runtime: "audiocraft"
2150
+ }
2151
+ });
2152
+ }
2153
+ return models;
2154
+ }
2155
+ };
2156
+
2157
+ // src/providers/openai-compat.ts
2158
+ var FETCH_TIMEOUT_MS4 = 5e3;
2159
+ var KNOWN_LOCAL_SERVERS = [
2160
+ { port: 8080, name: "llama.cpp / LocalAI", id: "llamacpp" },
2161
+ { port: 1234, name: "LM Studio", id: "lmstudio" },
2162
+ { port: 8e3, name: "vLLM", id: "vllm" },
2163
+ { port: 5e3, name: "TabbyAPI", id: "tabbyapi" },
2164
+ { port: 5001, name: "KoboldCpp", id: "koboldcpp" },
2165
+ { port: 1337, name: "Jan", id: "jan" }
2166
+ ];
2167
+ async function fetchJsonTimeout2(url, headers, timeoutMs = FETCH_TIMEOUT_MS4) {
2168
+ const controller = new AbortController();
2169
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2170
+ try {
2171
+ const res = await fetch(url, { signal: controller.signal, headers });
2172
+ if (!res.ok) return null;
2173
+ return await res.json();
2174
+ } catch {
2175
+ return null;
2176
+ } finally {
2177
+ clearTimeout(timer);
2178
+ }
2179
+ }
2180
+ var OpenAICompatProvider = class {
2181
+ id;
2182
+ name;
2183
+ modalities = ["llm"];
2184
+ isLocal = true;
2185
+ baseUrl;
2186
+ headers;
2187
+ constructor(config) {
2188
+ this.baseUrl = config.baseUrl.replace(/\/+$/, "");
2189
+ this.id = config.id ?? `openai-compat-${new URL(config.baseUrl).port}`;
2190
+ this.name = config.name ?? `OpenAI-Compatible (${this.baseUrl})`;
2191
+ this.headers = { "Content-Type": "application/json" };
2192
+ if (config.apiKey) {
2193
+ this.headers["Authorization"] = `Bearer ${config.apiKey}`;
2194
+ }
2195
+ }
2196
+ async ping() {
2197
+ const data = await fetchJsonTimeout2(`${this.baseUrl}/v1/models`, this.headers, 2e3);
2198
+ return data !== null;
2199
+ }
2200
+ async listModels(_modality) {
2201
+ if (_modality && _modality !== "llm") return [];
2202
+ const data = await fetchJsonTimeout2(`${this.baseUrl}/v1/models`, this.headers);
2203
+ if (!data?.data || !Array.isArray(data.data)) return [];
2204
+ const logo = getProviderLogo("openai");
2205
+ return data.data.map((m) => ({
2206
+ id: m.id,
2207
+ provider: this.id,
2208
+ name: m.id,
2209
+ modality: "llm",
2210
+ local: true,
2211
+ cost: { price: 0, unit: "free" },
2212
+ logo,
2213
+ status: "running",
2214
+ localInfo: {
2215
+ sizeBytes: 0,
2216
+ runtime: this.id
2217
+ },
2218
+ capabilities: {
2219
+ supportsStreaming: true
2220
+ }
2221
+ }));
2222
+ }
2223
+ async chat(options) {
2224
+ const start = Date.now();
2225
+ const model = options.model ?? "default";
2226
+ const body = {
2227
+ model,
2228
+ messages: options.messages,
2229
+ stream: false
2230
+ };
2231
+ if (options.temperature !== void 0) body.temperature = options.temperature;
2232
+ if (options.maxTokens !== void 0) body.max_tokens = options.maxTokens;
2233
+ if (options.jsonMode) body.response_format = { type: "json_object" };
2234
+ const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
2235
+ method: "POST",
2236
+ headers: this.headers,
2237
+ body: JSON.stringify(body)
2238
+ });
2239
+ if (!res.ok) throw new Error(`OpenAI-compat chat failed: ${res.status} ${await res.text()}`);
2240
+ const data = await res.json();
2241
+ const choice = data.choices?.[0];
2242
+ return {
2243
+ content: choice?.message?.content ?? "",
2244
+ provider: this.id,
2245
+ model,
2246
+ modality: "llm",
2247
+ latencyMs: Date.now() - start,
2248
+ usage: {
2249
+ cost: 0,
2250
+ input: data.usage?.prompt_tokens ?? 0,
2251
+ output: data.usage?.completion_tokens ?? 0,
2252
+ unit: "tokens"
2253
+ }
2254
+ };
2255
+ }
2256
+ stream(options) {
2257
+ const self = this;
2258
+ const start = Date.now();
2259
+ let aborted = false;
2260
+ let resolveResult = null;
2261
+ let rejectResult = null;
2262
+ const resultPromise = new Promise((resolve, reject) => {
2263
+ resolveResult = resolve;
2264
+ rejectResult = reject;
2265
+ });
2266
+ const model = options.model ?? "default";
2267
+ const body = {
2268
+ model,
2269
+ messages: options.messages,
2270
+ stream: true
2271
+ };
2272
+ if (options.temperature !== void 0) body.temperature = options.temperature;
2273
+ if (options.maxTokens !== void 0) body.max_tokens = options.maxTokens;
2274
+ const asyncIterator = {
2275
+ async *[Symbol.asyncIterator]() {
2276
+ try {
2277
+ const res = await fetch(`${self.baseUrl}/v1/chat/completions`, {
2278
+ method: "POST",
2279
+ headers: self.headers,
2280
+ body: JSON.stringify(body)
2281
+ });
2282
+ if (!res.ok) throw new Error(`Stream failed: ${res.status} ${await res.text()}`);
2283
+ const reader = res.body.getReader();
2284
+ const decoder = new TextDecoder();
2285
+ let fullContent = "";
2286
+ let buffer = "";
2287
+ while (!aborted) {
2288
+ const { done, value } = await reader.read();
2289
+ if (done) break;
2290
+ buffer += decoder.decode(value, { stream: true });
2291
+ const lines = buffer.split("\n");
2292
+ buffer = lines.pop() ?? "";
2293
+ for (const line of lines) {
2294
+ if (!line.startsWith("data: ") || line === "data: [DONE]") continue;
2295
+ try {
2296
+ const chunk = JSON.parse(line.slice(6));
2297
+ const delta = chunk.choices?.[0]?.delta?.content;
2298
+ if (delta) {
2299
+ fullContent += delta;
2300
+ yield { type: "text_delta", delta };
2301
+ }
2302
+ } catch {
2303
+ }
2304
+ }
2305
+ }
2306
+ const result = {
2307
+ content: fullContent,
2308
+ provider: self.id,
2309
+ model,
2310
+ modality: "llm",
2311
+ latencyMs: Date.now() - start,
2312
+ usage: { cost: 0, unit: "tokens" }
2313
+ };
2314
+ resolveResult?.(result);
2315
+ yield { type: "done", result };
2316
+ } catch (err) {
2317
+ const error = err instanceof Error ? err : new Error(String(err));
2318
+ rejectResult?.(error);
2319
+ yield { type: "error", error };
2320
+ }
2321
+ }
2322
+ };
2323
+ return {
2324
+ [Symbol.asyncIterator]: () => asyncIterator[Symbol.asyncIterator](),
2325
+ result: () => resultPromise,
2326
+ abort: () => {
2327
+ aborted = true;
2328
+ }
2329
+ };
2330
+ }
2331
+ };
2332
+ async function detectOpenAICompatServers() {
2333
+ const providers = [];
2334
+ const results = await Promise.allSettled(
2335
+ KNOWN_LOCAL_SERVERS.map(async (server) => {
2336
+ const baseUrl = `http://localhost:${server.port}`;
2337
+ const provider = new OpenAICompatProvider({
2338
+ baseUrl,
2339
+ name: server.name,
2340
+ id: server.id
2341
+ });
2342
+ const ok = await provider.ping();
2343
+ if (ok) return provider;
2344
+ return null;
2345
+ })
2346
+ );
2347
+ for (const result of results) {
2348
+ if (result.status === "fulfilled" && result.value) {
2349
+ providers.push(result.value);
2350
+ }
2351
+ }
2352
+ return providers;
2353
+ }
2354
+
1394
2355
  // src/noosphere.ts
1395
2356
  var Noosphere = class {
1396
2357
  config;
@@ -1581,6 +2542,30 @@ var Noosphere = class {
1581
2542
  getUsage(options) {
1582
2543
  return this.tracker.getSummary(options);
1583
2544
  }
2545
+ // --- Local Model Management ---
2546
+ async installModel(name) {
2547
+ if (!this.initialized) await this.init();
2548
+ const provider = this.registry.getProvider("ollama");
2549
+ if (!provider) throw new NoosphereError("Ollama provider not available", { code: "PROVIDER_UNAVAILABLE", provider: "ollama", modality: "llm" });
2550
+ return provider.pullModel(name);
2551
+ }
2552
+ async uninstallModel(name) {
2553
+ if (!this.initialized) await this.init();
2554
+ const provider = this.registry.getProvider("ollama");
2555
+ if (!provider) throw new NoosphereError("Ollama provider not available", { code: "PROVIDER_UNAVAILABLE", provider: "ollama", modality: "llm" });
2556
+ await provider.deleteModel(name);
2557
+ }
2558
+ async getHardware() {
2559
+ if (!this.initialized) await this.init();
2560
+ const provider = this.registry.getProvider("ollama");
2561
+ if (!provider) return { ollama: false, runningModels: [] };
2562
+ try {
2563
+ const runningModels = await provider.getRunningModels();
2564
+ return { ollama: true, runningModels };
2565
+ } catch {
2566
+ return { ollama: false, runningModels: [] };
2567
+ }
2568
+ }
1584
2569
  // --- Lifecycle ---
1585
2570
  async dispose() {
1586
2571
  for (const provider of this.registry.getAllProviders()) {
@@ -1631,10 +2616,21 @@ var Noosphere = class {
1631
2616
  return false;
1632
2617
  }
1633
2618
  };
2619
+ const ollamaCfg = local["ollama"];
1634
2620
  const comfyuiCfg = local["comfyui"];
1635
2621
  const piperCfg = local["piper"];
1636
2622
  const kokoroCfg = local["kokoro"];
1637
2623
  await Promise.allSettled([
2624
+ // Ollama — auto-detect even without explicit config
2625
+ (async () => {
2626
+ const host = ollamaCfg?.host ?? "http://localhost";
2627
+ const port = ollamaCfg?.port ?? 11434;
2628
+ const provider = new OllamaProvider({ host, port });
2629
+ const ok = await provider.ping();
2630
+ if (ok) {
2631
+ this.registry.addProvider(provider);
2632
+ }
2633
+ })(),
1638
2634
  // ComfyUI
1639
2635
  (async () => {
1640
2636
  if (comfyuiCfg?.enabled) {
@@ -1661,6 +2657,29 @@ var Noosphere = class {
1661
2657
  this.registry.addProvider(new LocalTTSProvider({ id: "kokoro", name: "Kokoro TTS", host: kokoroCfg.host, port: kokoroCfg.port }));
1662
2658
  }
1663
2659
  }
2660
+ })(),
2661
+ // HuggingFace local model catalog
2662
+ (async () => {
2663
+ this.registry.addProvider(new HfLocalProvider());
2664
+ })(),
2665
+ // Whisper local STT
2666
+ (async () => {
2667
+ const whisper = new WhisperLocalProvider();
2668
+ const ok = await whisper.ping();
2669
+ if (ok) this.registry.addProvider(whisper);
2670
+ })(),
2671
+ // AudioCraft local music generation
2672
+ (async () => {
2673
+ const audiocraft = new AudioCraftProvider();
2674
+ const ok = await audiocraft.ping();
2675
+ if (ok) this.registry.addProvider(audiocraft);
2676
+ })(),
2677
+ // Auto-detect OpenAI-compatible servers
2678
+ (async () => {
2679
+ const servers = await detectOpenAICompatServers();
2680
+ for (const server of servers) {
2681
+ this.registry.addProvider(server);
2682
+ }
1664
2683
  })()
1665
2684
  ]);
1666
2685
  }
@@ -1752,10 +2771,16 @@ var Noosphere = class {
1752
2771
  }
1753
2772
  };
1754
2773
  export {
2774
+ AudioCraftProvider,
2775
+ HfLocalProvider,
1755
2776
  Noosphere,
1756
2777
  NoosphereError,
2778
+ OllamaProvider,
2779
+ OpenAICompatProvider,
1757
2780
  PROVIDER_IDS,
1758
2781
  PROVIDER_LOGOS,
2782
+ WhisperLocalProvider,
2783
+ detectOpenAICompatServers,
1759
2784
  getAllProviderLogos,
1760
2785
  getProviderLogo
1761
2786
  };