noosphere 0.7.0 → 0.8.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.d.cts CHANGED
@@ -92,6 +92,7 @@ interface ModelInfo {
92
92
  id: string;
93
93
  provider: string;
94
94
  name: string;
95
+ description?: string;
95
96
  modality: Modality;
96
97
  local: boolean;
97
98
  cost: {
@@ -317,7 +318,7 @@ declare class Noosphere {
317
318
  getProviders(modality?: Modality): Promise<ProviderInfo[]>;
318
319
  getModels(modality?: Modality): Promise<ModelInfo[]>;
319
320
  getModel(provider: string, modelId: string): Promise<ModelInfo | null>;
320
- syncModels(): Promise<SyncResult>;
321
+ syncModels(modality?: Modality): Promise<SyncResult>;
321
322
  getUsage(options?: UsageQueryOptions): UsageSummary;
322
323
  installModel(name: string): Promise<AsyncGenerator<OllamaPullProgress>>;
323
324
  uninstallModel(name: string): Promise<void>;
package/dist/index.d.ts CHANGED
@@ -92,6 +92,7 @@ interface ModelInfo {
92
92
  id: string;
93
93
  provider: string;
94
94
  name: string;
95
+ description?: string;
95
96
  modality: Modality;
96
97
  local: boolean;
97
98
  cost: {
@@ -317,7 +318,7 @@ declare class Noosphere {
317
318
  getProviders(modality?: Modality): Promise<ProviderInfo[]>;
318
319
  getModels(modality?: Modality): Promise<ModelInfo[]>;
319
320
  getModel(provider: string, modelId: string): Promise<ModelInfo | null>;
320
- syncModels(): Promise<SyncResult>;
321
+ syncModels(modality?: Modality): Promise<SyncResult>;
321
322
  getUsage(options?: UsageQueryOptions): UsageSummary;
322
323
  installModel(name: string): Promise<AsyncGenerator<OllamaPullProgress>>;
323
324
  uninstallModel(name: string): Promise<void>;
package/dist/index.js CHANGED
@@ -312,20 +312,22 @@ var Registry = class {
312
312
  const cached = this.modelCache.get(provider);
313
313
  return cached?.models.find((m) => m.id === modelId) ?? null;
314
314
  }
315
- async syncProvider(providerId) {
315
+ async syncProvider(providerId, modality) {
316
316
  const provider = this.providers.get(providerId);
317
317
  if (!provider) return 0;
318
- const models = await provider.listModels();
318
+ if (modality && !provider.modalities.includes(modality)) return 0;
319
+ const models = await provider.listModels(modality);
319
320
  this.modelCache.set(providerId, { models, syncedAt: Date.now() });
320
321
  return models.length;
321
322
  }
322
- async syncAll() {
323
+ async syncAll(modality) {
323
324
  const byProvider = {};
324
325
  const errors = [];
325
326
  let synced = 0;
326
327
  for (const provider of this.providers.values()) {
328
+ if (modality && !provider.modalities.includes(modality)) continue;
327
329
  try {
328
- const count = await this.syncProvider(provider.id);
330
+ const count = await this.syncProvider(provider.id, modality);
329
331
  byProvider[provider.id] = count;
330
332
  synced += count;
331
333
  } catch (err) {
@@ -1110,6 +1112,7 @@ var ComfyUIProvider = class {
1110
1112
  id: `civitai-${item.id}`,
1111
1113
  provider: "comfyui",
1112
1114
  name: item.name ?? `CivitAI Model ${item.id}`,
1115
+ description: item.description ? item.description.replace(/<[^>]+>/g, "").trim().slice(0, 300) || void 0 : void 0,
1113
1116
  modality: "image",
1114
1117
  local: true,
1115
1118
  cost: { price: 0, unit: "free" },
@@ -1548,6 +1551,28 @@ async function fetchJson(url, options) {
1548
1551
  clearTimeout(timer);
1549
1552
  }
1550
1553
  }
1554
+ async function fetchOllamaDescriptions() {
1555
+ const controller = new AbortController();
1556
+ const timer = setTimeout(() => controller.abort(), 5e3);
1557
+ try {
1558
+ const res = await fetch("https://ollama.com/library", { signal: controller.signal });
1559
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1560
+ const html = await res.text();
1561
+ const descriptions = /* @__PURE__ */ new Map();
1562
+ const cardRegex = /href="\/library\/([^"]+)"[\s\S]*?<p[^>]*>([\s\S]*?)<\/p>/g;
1563
+ let match;
1564
+ while ((match = cardRegex.exec(html)) !== null) {
1565
+ const modelName = match[1].trim();
1566
+ const desc = match[2].replace(/<[^>]*>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/\s+/g, " ").trim();
1567
+ if (modelName && desc) {
1568
+ descriptions.set(modelName, desc);
1569
+ }
1570
+ }
1571
+ return descriptions;
1572
+ } finally {
1573
+ clearTimeout(timer);
1574
+ }
1575
+ }
1551
1576
  var OllamaProvider = class {
1552
1577
  id = "ollama";
1553
1578
  name = "Ollama (Local)";
@@ -1571,10 +1596,11 @@ var OllamaProvider = class {
1571
1596
  }
1572
1597
  async listModels(_modality) {
1573
1598
  if (_modality && _modality !== "llm") return [];
1574
- const [localData, catalogData, runningData] = await Promise.all([
1599
+ const [localData, catalogData, runningData, descriptions] = await Promise.all([
1575
1600
  fetchJson(`${this.baseUrl}/api/tags`, { timeoutMs: 5e3 }).catch(() => null),
1576
1601
  fetchJson("https://ollama.com/api/tags", { timeoutMs: 5e3 }).catch(() => null),
1577
- fetchJson(`${this.baseUrl}/api/ps`, { timeoutMs: 5e3 }).catch(() => null)
1602
+ fetchJson(`${this.baseUrl}/api/ps`, { timeoutMs: 5e3 }).catch(() => null),
1603
+ fetchOllamaDescriptions().catch(() => /* @__PURE__ */ new Map())
1578
1604
  ]);
1579
1605
  const runningNames = /* @__PURE__ */ new Set();
1580
1606
  if (runningData?.models) {
@@ -1587,27 +1613,30 @@ var OllamaProvider = class {
1587
1613
  if (localData?.models) {
1588
1614
  for (const m of localData.models) {
1589
1615
  const isRunning = runningNames.has(m.name) || runningNames.has(m.model);
1590
- models.set(m.name, this.toModelInfo(m, isRunning ? "running" : "installed", true));
1616
+ models.set(m.name, this.toModelInfo(m, isRunning ? "running" : "installed", true, descriptions));
1591
1617
  }
1592
1618
  }
1593
1619
  if (catalogData?.models) {
1594
1620
  for (const m of catalogData.models) {
1595
1621
  const name = m.name;
1596
1622
  if (!models.has(name)) {
1597
- models.set(name, this.toModelInfo(m, "available", false));
1623
+ models.set(name, this.toModelInfo(m, "available", false, descriptions));
1598
1624
  }
1599
1625
  }
1600
1626
  }
1601
1627
  return Array.from(models.values());
1602
1628
  }
1603
- toModelInfo(m, status, isLocal) {
1629
+ toModelInfo(m, status, isLocal, descriptions) {
1604
1630
  const name = m.name ?? m.model ?? "unknown";
1605
1631
  const family = m.details?.family;
1606
1632
  const logoProvider = inferLogoProvider(name, family);
1633
+ const baseName = name.split(":")[0];
1634
+ const description = descriptions?.get(baseName);
1607
1635
  return {
1608
1636
  id: name,
1609
1637
  provider: "ollama",
1610
1638
  name,
1639
+ ...description ? { description } : {},
1611
1640
  modality: "llm",
1612
1641
  local: true,
1613
1642
  cost: { price: 0, unit: "free" },
@@ -1816,16 +1845,79 @@ var OllamaProvider = class {
1816
1845
  }
1817
1846
  };
1818
1847
 
1848
+ // src/utils/parse-readme.ts
1849
+ async function fetchReadmeDescription(modelId, timeoutMs = 5e3) {
1850
+ try {
1851
+ const controller = new AbortController();
1852
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1853
+ try {
1854
+ const res = await fetch(
1855
+ `https://huggingface.co/${modelId}/raw/main/README.md`,
1856
+ { signal: controller.signal }
1857
+ );
1858
+ if (!res.ok) return void 0;
1859
+ const text = await res.text();
1860
+ return parseReadmeDescription(text);
1861
+ } finally {
1862
+ clearTimeout(timer);
1863
+ }
1864
+ } catch {
1865
+ return void 0;
1866
+ }
1867
+ }
1868
+ function parseReadmeDescription(readme) {
1869
+ const withoutFrontmatter = readme.replace(/^---[\s\S]*?---\s*/, "");
1870
+ const lines = withoutFrontmatter.split("\n");
1871
+ let paragraph = "";
1872
+ for (const line of lines) {
1873
+ const trimmed = line.trim();
1874
+ if (!trimmed) {
1875
+ if (paragraph) break;
1876
+ continue;
1877
+ }
1878
+ if (trimmed.startsWith("#")) {
1879
+ if (paragraph) break;
1880
+ continue;
1881
+ }
1882
+ if (/^\[?!\[/.test(trimmed) || /^</.test(trimmed)) continue;
1883
+ if (/^\[.*\]\(.*\)$/.test(trimmed)) continue;
1884
+ paragraph += (paragraph ? " " : "") + trimmed;
1885
+ }
1886
+ if (!paragraph) return void 0;
1887
+ paragraph = paragraph.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/<[^>]+>/g, "").trim();
1888
+ if (!paragraph) return void 0;
1889
+ if (paragraph.length > 300) paragraph = paragraph.slice(0, 297) + "...";
1890
+ return paragraph;
1891
+ }
1892
+
1819
1893
  // src/providers/hf-local.ts
1820
1894
  import { readdir, readFile } from "fs/promises";
1821
1895
  import { join } from "path";
1822
1896
  import { homedir } from "os";
1823
1897
  var FETCH_TIMEOUT_MS3 = 5e3;
1824
1898
  var HF_HUB_API2 = "https://huggingface.co/api/models";
1899
+ var HF_ORG_TO_LOGO_PROVIDER = {
1900
+ "meta-llama": "meta",
1901
+ "facebook": "meta",
1902
+ "google": "google",
1903
+ "microsoft": "microsoft",
1904
+ "nvidia": "nvidia",
1905
+ "mistralai": "mistral",
1906
+ "Qwen": "qwen",
1907
+ "deepseek-ai": "deepseek",
1908
+ "openai": "openai",
1909
+ "CohereForAI": "cohere",
1910
+ "rhasspy": "piper",
1911
+ "stabilityai": "huggingface",
1912
+ "black-forest-labs": "huggingface",
1913
+ "tiiuae": "huggingface",
1914
+ "allenai": "huggingface",
1915
+ "Salesforce": "huggingface"
1916
+ };
1825
1917
  var PIPELINE_TAG_TO_MODALITY = {
1826
1918
  "text-to-image": "image",
1827
1919
  "text-to-video": "video",
1828
- "text-to-audio": "tts",
1920
+ "text-to-audio": "music",
1829
1921
  "text-to-speech": "tts",
1830
1922
  "automatic-speech-recognition": "stt"
1831
1923
  };
@@ -1853,7 +1945,7 @@ async function fetchJsonTimeout(url, timeoutMs = FETCH_TIMEOUT_MS3) {
1853
1945
  var HfLocalProvider = class {
1854
1946
  id = "hf-local";
1855
1947
  name = "HuggingFace Local Models";
1856
- modalities = ["image", "video", "tts", "stt"];
1948
+ modalities = ["image", "video", "tts", "stt", "music"];
1857
1949
  isLocal = true;
1858
1950
  cachedModels = null;
1859
1951
  async ping() {
@@ -1875,8 +1967,7 @@ var HfLocalProvider = class {
1875
1967
  }
1876
1968
  async fetchCatalog() {
1877
1969
  const seen = /* @__PURE__ */ new Set();
1878
- const models = [];
1879
- const logo = getProviderLogo("huggingface");
1970
+ const entries = [];
1880
1971
  const results = await Promise.allSettled(
1881
1972
  CATALOG_QUERIES.map(async (q) => {
1882
1973
  const params = new URLSearchParams({
@@ -1894,32 +1985,56 @@ var HfLocalProvider = class {
1894
1985
  const id = entry.id ?? entry.modelId;
1895
1986
  if (!id || seen.has(id)) continue;
1896
1987
  seen.add(id);
1897
- const pipelineTag = entry.pipeline_tag ?? "";
1898
- const modality = PIPELINE_TAG_TO_MODALITY[pipelineTag] ?? "image";
1899
- models.push({
1988
+ entries.push({
1900
1989
  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: {}
1990
+ pipelineTag: entry.pipeline_tag ?? "",
1991
+ libraryName: entry.library_name
1914
1992
  });
1915
1993
  }
1916
1994
  }
1995
+ const descriptionMap = /* @__PURE__ */ new Map();
1996
+ for (let i = 0; i < entries.length; i += 10) {
1997
+ const batch = entries.slice(i, i + 10);
1998
+ const descs = await Promise.allSettled(
1999
+ batch.map(async (e) => {
2000
+ const desc = await fetchReadmeDescription(e.id);
2001
+ return { id: e.id, desc };
2002
+ })
2003
+ );
2004
+ for (const d of descs) {
2005
+ if (d.status === "fulfilled" && d.value.desc) {
2006
+ descriptionMap.set(d.value.id, d.value.desc);
2007
+ }
2008
+ }
2009
+ }
2010
+ const models = [];
2011
+ for (const e of entries) {
2012
+ const modality = PIPELINE_TAG_TO_MODALITY[e.pipelineTag] ?? "image";
2013
+ const org = e.id.includes("/") ? e.id.split("/")[0] : void 0;
2014
+ const logoProvider = org ? HF_ORG_TO_LOGO_PROVIDER[org] ?? "huggingface" : "huggingface";
2015
+ models.push({
2016
+ id: e.id,
2017
+ provider: "hf-local",
2018
+ name: e.id.split("/").pop() ?? e.id,
2019
+ modality,
2020
+ local: true,
2021
+ cost: { price: 0, unit: "free" },
2022
+ logo: getProviderLogo(logoProvider),
2023
+ description: descriptionMap.get(e.id),
2024
+ status: "available",
2025
+ localInfo: {
2026
+ sizeBytes: 0,
2027
+ runtime: "huggingface",
2028
+ family: e.libraryName
2029
+ },
2030
+ capabilities: {}
2031
+ });
2032
+ }
1917
2033
  return models;
1918
2034
  }
1919
2035
  async scanLocalCache() {
1920
2036
  const models = [];
1921
2037
  const cacheDir = join(homedir(), ".cache", "huggingface", "hub");
1922
- const logo = getProviderLogo("huggingface");
1923
2038
  try {
1924
2039
  const entries = await readdir(cacheDir, { withFileTypes: true });
1925
2040
  for (const entry of entries) {
@@ -1951,6 +2066,8 @@ var HfLocalProvider = class {
1951
2066
  }
1952
2067
  }
1953
2068
  const modality = PIPELINE_TAG_TO_MODALITY[pipelineTag] ?? "image";
2069
+ const org = modelId.includes("/") ? modelId.split("/")[0] : void 0;
2070
+ const logoProvider = org ? HF_ORG_TO_LOGO_PROVIDER[org] ?? "huggingface" : "huggingface";
1954
2071
  models.push({
1955
2072
  id: modelId,
1956
2073
  provider: "hf-local",
@@ -1958,7 +2075,7 @@ var HfLocalProvider = class {
1958
2075
  modality,
1959
2076
  local: true,
1960
2077
  cost: { price: 0, unit: "free" },
1961
- logo,
2078
+ logo: getProviderLogo(logoProvider),
1962
2079
  status: "installed",
1963
2080
  localInfo: {
1964
2081
  sizeBytes: 0,
@@ -1979,6 +2096,25 @@ import { access } from "fs/promises";
1979
2096
  import { join as join2 } from "path";
1980
2097
  import { homedir as homedir2 } from "os";
1981
2098
  var WHISPER_MODELS = ["tiny", "base", "small", "medium", "large", "large-v2", "large-v3", "turbo"];
2099
+ var WHISPER_HF_REPOS = {
2100
+ "tiny": "openai/whisper-tiny",
2101
+ "base": "openai/whisper-base",
2102
+ "small": "openai/whisper-small",
2103
+ "medium": "openai/whisper-medium",
2104
+ "large": "openai/whisper-large",
2105
+ "large-v2": "openai/whisper-large-v2",
2106
+ "large-v3": "openai/whisper-large-v3",
2107
+ "turbo": "openai/whisper-large-v3-turbo"
2108
+ };
2109
+ async function fetchWhisperDescriptions() {
2110
+ const descriptions = /* @__PURE__ */ new Map();
2111
+ const fetches = Object.entries(WHISPER_HF_REPOS).map(async ([size, repo]) => {
2112
+ const desc = await fetchReadmeDescription(repo, 8e3);
2113
+ if (desc) descriptions.set(size, desc);
2114
+ });
2115
+ await Promise.allSettled(fetches);
2116
+ return descriptions;
2117
+ }
1982
2118
  function runPython(code, timeoutMs = 5e3) {
1983
2119
  return new Promise((resolve, reject) => {
1984
2120
  const proc = execFile("python3", ["-c", code], { timeout: timeoutMs }, (err, stdout) => {
@@ -2024,7 +2160,8 @@ var WhisperLocalProvider = class {
2024
2160
  if (_modality && _modality !== "stt") return [];
2025
2161
  const runtime = await this.detectRuntime();
2026
2162
  if (!runtime) return [];
2027
- const logo = getProviderLogo("huggingface");
2163
+ const descMap = await fetchWhisperDescriptions().catch(() => /* @__PURE__ */ new Map());
2164
+ const logo = getProviderLogo("openai");
2028
2165
  const models = [];
2029
2166
  for (const name of WHISPER_MODELS) {
2030
2167
  const installed = await this.isModelCached(name, runtime);
@@ -2032,6 +2169,7 @@ var WhisperLocalProvider = class {
2032
2169
  id: `whisper-${name}`,
2033
2170
  provider: "whisper-local",
2034
2171
  name: `Whisper ${name}`,
2172
+ description: descMap.get(name),
2035
2173
  modality: "stt",
2036
2174
  local: true,
2037
2175
  cost: { price: 0, unit: "free" },
@@ -2095,6 +2233,22 @@ var AUDIOCRAFT_MODELS = [
2095
2233
  { id: "musicgen-melody", name: "MusicGen Melody" },
2096
2234
  { id: "audiogen-medium", name: "AudioGen Medium" }
2097
2235
  ];
2236
+ var AUDIOCRAFT_HF_REPOS = {
2237
+ "musicgen-small": "facebook/musicgen-small",
2238
+ "musicgen-medium": "facebook/musicgen-medium",
2239
+ "musicgen-large": "facebook/musicgen-large",
2240
+ "musicgen-melody": "facebook/musicgen-melody",
2241
+ "audiogen-medium": "facebook/audiogen-medium"
2242
+ };
2243
+ async function fetchAudioCraftDescriptions() {
2244
+ const descriptions = /* @__PURE__ */ new Map();
2245
+ const fetches = Object.entries(AUDIOCRAFT_HF_REPOS).map(async ([id, repo]) => {
2246
+ const desc = await fetchReadmeDescription(repo, 8e3);
2247
+ if (desc) descriptions.set(id, desc);
2248
+ });
2249
+ await Promise.allSettled(fetches);
2250
+ return descriptions;
2251
+ }
2098
2252
  function runPython2(code, timeoutMs = 5e3) {
2099
2253
  return new Promise((resolve, reject) => {
2100
2254
  execFile2("python3", ["-c", code], { timeout: timeoutMs }, (err, stdout) => {
@@ -2130,6 +2284,7 @@ var AudioCraftProvider = class {
2130
2284
  async listModels(_modality) {
2131
2285
  if (_modality && _modality !== "music") return [];
2132
2286
  if (!await this.ping()) return [];
2287
+ const descMap = await fetchAudioCraftDescriptions().catch(() => /* @__PURE__ */ new Map());
2133
2288
  const logo = getProviderLogo("meta");
2134
2289
  const models = [];
2135
2290
  for (const m of AUDIOCRAFT_MODELS) {
@@ -2139,6 +2294,7 @@ var AudioCraftProvider = class {
2139
2294
  id: m.id,
2140
2295
  provider: "audiocraft",
2141
2296
  name: m.name,
2297
+ description: descMap.get(m.id),
2142
2298
  modality: "music",
2143
2299
  local: true,
2144
2300
  cost: { price: 0, unit: "free" },
@@ -2534,9 +2690,9 @@ var Noosphere = class {
2534
2690
  if (!this.initialized) await this.init();
2535
2691
  return this.registry.getModel(provider, modelId);
2536
2692
  }
2537
- async syncModels() {
2693
+ async syncModels(modality) {
2538
2694
  if (!this.initialized) await this.init();
2539
- return this.registry.syncAll();
2695
+ return this.registry.syncAll(modality);
2540
2696
  }
2541
2697
  // --- Tracking Methods ---
2542
2698
  getUsage(options) {