noosphere 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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" },
@@ -2352,6 +2508,501 @@ async function detectOpenAICompatServers() {
2352
2508
  return providers;
2353
2509
  }
2354
2510
 
2511
+ // src/providers/openai-media.ts
2512
+ var OPENAI_API_BASE = "https://api.openai.com/v1";
2513
+ var FETCH_TIMEOUT_MS5 = 8e3;
2514
+ var MODEL_PREFIX_MAP = [
2515
+ { prefix: "dall-e-", modality: "image" },
2516
+ { prefix: "gpt-image-", modality: "image" },
2517
+ { prefix: "sora-", modality: "video" },
2518
+ { prefix: "tts-", modality: "tts" },
2519
+ { prefix: "whisper-", modality: "stt" }
2520
+ ];
2521
+ function classifyModel(id) {
2522
+ for (const { prefix, modality } of MODEL_PREFIX_MAP) {
2523
+ if (id.startsWith(prefix)) return modality;
2524
+ }
2525
+ return null;
2526
+ }
2527
+ var OpenAIMediaProvider = class {
2528
+ constructor(apiKey) {
2529
+ this.apiKey = apiKey;
2530
+ }
2531
+ id = "openai-media";
2532
+ name = "OpenAI (Image, Video, TTS, STT)";
2533
+ modalities = ["image", "video", "tts", "stt"];
2534
+ isLocal = false;
2535
+ modelsCache = null;
2536
+ async ping() {
2537
+ try {
2538
+ const controller = new AbortController();
2539
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS5);
2540
+ try {
2541
+ const res = await fetch(`${OPENAI_API_BASE}/models`, {
2542
+ headers: { Authorization: `Bearer ${this.apiKey}` },
2543
+ signal: controller.signal
2544
+ });
2545
+ return res.ok;
2546
+ } finally {
2547
+ clearTimeout(timer);
2548
+ }
2549
+ } catch {
2550
+ return false;
2551
+ }
2552
+ }
2553
+ async listModels(modality) {
2554
+ if (this.modelsCache) {
2555
+ return modality ? this.modelsCache.filter((m) => m.modality === modality) : this.modelsCache;
2556
+ }
2557
+ try {
2558
+ const controller = new AbortController();
2559
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS5);
2560
+ let data;
2561
+ try {
2562
+ const res = await fetch(`${OPENAI_API_BASE}/models`, {
2563
+ headers: { Authorization: `Bearer ${this.apiKey}` },
2564
+ signal: controller.signal
2565
+ });
2566
+ if (!res.ok) return [];
2567
+ data = await res.json();
2568
+ } finally {
2569
+ clearTimeout(timer);
2570
+ }
2571
+ const entries = data?.data ?? [];
2572
+ const logo = getProviderLogo("openai");
2573
+ const models = [];
2574
+ for (const entry of entries) {
2575
+ const mod = classifyModel(entry.id);
2576
+ if (!mod) continue;
2577
+ const info = {
2578
+ id: entry.id,
2579
+ provider: "openai-media",
2580
+ name: entry.id,
2581
+ modality: mod,
2582
+ local: false,
2583
+ cost: { price: 0, unit: "per_request" },
2584
+ logo,
2585
+ description: entry.description,
2586
+ capabilities: this.getCapabilities(entry.id, mod)
2587
+ };
2588
+ models.push(info);
2589
+ }
2590
+ this.modelsCache = models;
2591
+ return modality ? models.filter((m) => m.modality === modality) : models;
2592
+ } catch {
2593
+ return [];
2594
+ }
2595
+ }
2596
+ async image(options) {
2597
+ const model = options.model ?? "gpt-image-1";
2598
+ const width = options.width ?? 1024;
2599
+ const height = options.height ?? 1024;
2600
+ const start = Date.now();
2601
+ const isGptImage = model.startsWith("gpt-image-");
2602
+ const body = {
2603
+ model,
2604
+ prompt: options.prompt,
2605
+ n: 1,
2606
+ size: `${width}x${height}`
2607
+ };
2608
+ if (!isGptImage) {
2609
+ body.response_format = "url";
2610
+ }
2611
+ const res = await fetch(`${OPENAI_API_BASE}/images/generations`, {
2612
+ method: "POST",
2613
+ headers: {
2614
+ "Content-Type": "application/json",
2615
+ Authorization: `Bearer ${this.apiKey}`
2616
+ },
2617
+ body: JSON.stringify(body)
2618
+ });
2619
+ if (!res.ok) {
2620
+ const errorBody = await res.text();
2621
+ throw new Error(`OpenAI image generation failed (${res.status}): ${errorBody}`);
2622
+ }
2623
+ const data = await res.json();
2624
+ const item = data?.data?.[0];
2625
+ const result = {
2626
+ provider: "openai-media",
2627
+ model,
2628
+ modality: "image",
2629
+ latencyMs: Date.now() - start,
2630
+ usage: {
2631
+ cost: 0,
2632
+ unit: "per_image"
2633
+ },
2634
+ media: {
2635
+ width,
2636
+ height,
2637
+ format: "png"
2638
+ }
2639
+ };
2640
+ if (item?.b64_json) {
2641
+ result.buffer = Buffer.from(item.b64_json, "base64");
2642
+ } else if (item?.url) {
2643
+ result.url = item.url;
2644
+ }
2645
+ return result;
2646
+ }
2647
+ async speak(options) {
2648
+ const model = options.model ?? "tts-1";
2649
+ const voice = options.voice ?? "alloy";
2650
+ const format = options.format ?? "mp3";
2651
+ const speed = options.speed ?? 1;
2652
+ const start = Date.now();
2653
+ const body = {
2654
+ model,
2655
+ input: options.text,
2656
+ voice,
2657
+ response_format: format,
2658
+ speed
2659
+ };
2660
+ const res = await fetch(`${OPENAI_API_BASE}/audio/speech`, {
2661
+ method: "POST",
2662
+ headers: {
2663
+ "Content-Type": "application/json",
2664
+ Authorization: `Bearer ${this.apiKey}`
2665
+ },
2666
+ body: JSON.stringify(body)
2667
+ });
2668
+ if (!res.ok) {
2669
+ const errorBody = await res.text();
2670
+ throw new Error(`OpenAI TTS failed (${res.status}): ${errorBody}`);
2671
+ }
2672
+ const arrayBuffer = await res.arrayBuffer();
2673
+ const buffer = Buffer.from(arrayBuffer);
2674
+ return {
2675
+ buffer,
2676
+ provider: "openai-media",
2677
+ model,
2678
+ modality: "tts",
2679
+ latencyMs: Date.now() - start,
2680
+ usage: {
2681
+ cost: 0,
2682
+ input: options.text.length,
2683
+ unit: "per_1k_chars"
2684
+ },
2685
+ media: {
2686
+ format
2687
+ }
2688
+ };
2689
+ }
2690
+ async video(options) {
2691
+ const model = options.model ?? "sora-2";
2692
+ const start = Date.now();
2693
+ const body = {
2694
+ model,
2695
+ prompt: options.prompt,
2696
+ n: 1
2697
+ };
2698
+ if (options.duration) body.duration = options.duration;
2699
+ if (options.width && options.height) body.size = `${options.width}x${options.height}`;
2700
+ const res = await fetch(`${OPENAI_API_BASE}/videos/generations`, {
2701
+ method: "POST",
2702
+ headers: {
2703
+ "Content-Type": "application/json",
2704
+ Authorization: `Bearer ${this.apiKey}`
2705
+ },
2706
+ body: JSON.stringify(body)
2707
+ });
2708
+ if (!res.ok) {
2709
+ const errorBody = await res.text();
2710
+ throw new Error(`OpenAI video generation failed (${res.status}): ${errorBody}`);
2711
+ }
2712
+ const data = await res.json();
2713
+ const videoUrl = data?.data?.[0]?.url;
2714
+ return {
2715
+ url: videoUrl,
2716
+ provider: "openai-media",
2717
+ model,
2718
+ modality: "video",
2719
+ latencyMs: Date.now() - start,
2720
+ usage: {
2721
+ cost: 0,
2722
+ unit: "per_video"
2723
+ },
2724
+ media: {
2725
+ duration: options.duration,
2726
+ width: options.width,
2727
+ height: options.height
2728
+ }
2729
+ };
2730
+ }
2731
+ getCapabilities(id, modality) {
2732
+ if (modality === "image") {
2733
+ return {
2734
+ maxWidth: id.startsWith("dall-e-3") ? 1792 : 1024,
2735
+ maxHeight: id.startsWith("dall-e-3") ? 1792 : 1024
2736
+ };
2737
+ }
2738
+ if (modality === "tts") {
2739
+ return {
2740
+ voices: ["alloy", "ash", "coral", "echo", "fable", "onyx", "nova", "sage", "shimmer"]
2741
+ };
2742
+ }
2743
+ if (modality === "video") {
2744
+ return {
2745
+ maxDuration: id.includes("pro") ? 20 : 10,
2746
+ supportsStreaming: false
2747
+ };
2748
+ }
2749
+ if (modality === "stt") {
2750
+ return {
2751
+ languages: ["en", "zh", "de", "es", "ru", "ko", "fr", "ja", "pt", "tr", "pl", "ca", "nl", "ar", "sv", "it", "id", "hi", "fi", "vi", "he", "uk", "el", "ms", "cs", "ro", "da", "hu", "ta", "no", "th", "ur", "hr", "bg", "lt", "la", "mi", "ml", "cy", "sk", "te", "fa", "lv", "bn", "sr", "az", "sl", "kn", "et", "mk", "br", "eu", "is", "hy", "ne", "mn", "bs", "kk", "sq", "sw", "gl", "mr", "pa", "si", "km", "sn", "yo", "so", "af", "oc", "ka", "be", "tg", "sd", "gu", "am", "yi", "lo", "uz", "fo", "ht", "ps", "tk", "nn", "mt", "sa", "lb", "my", "bo", "tl", "mg", "as", "tt", "haw", "ln", "ha", "ba", "jw", "su"]
2752
+ };
2753
+ }
2754
+ return void 0;
2755
+ }
2756
+ };
2757
+
2758
+ // src/providers/google-media.ts
2759
+ var GOOGLE_API_BASE = "https://generativelanguage.googleapis.com/v1beta";
2760
+ var FETCH_TIMEOUT_MS6 = 8e3;
2761
+ var GOOGLE_TTS_VOICES = [
2762
+ "Aoede",
2763
+ "Charon",
2764
+ "Fenrir",
2765
+ "Kore",
2766
+ "Puck",
2767
+ "Leda",
2768
+ "Orus",
2769
+ "Perseus",
2770
+ "Zephyr",
2771
+ "Callirrhoe"
2772
+ ];
2773
+ function classifyGoogleModel(model) {
2774
+ const name = (model.name ?? "").replace("models/", "");
2775
+ const methods = model.supportedGenerationMethods ?? [];
2776
+ if (name.startsWith("imagen") && methods.includes("predict")) return "image";
2777
+ if (name.startsWith("veo") && methods.includes("predictLongRunning")) return "video";
2778
+ if (name.includes("-tts") && methods.includes("generateContent")) return "tts";
2779
+ return null;
2780
+ }
2781
+ var GoogleMediaProvider = class {
2782
+ constructor(apiKey) {
2783
+ this.apiKey = apiKey;
2784
+ }
2785
+ id = "google-media";
2786
+ name = "Google (Image, Video, TTS)";
2787
+ modalities = ["image", "video", "tts"];
2788
+ isLocal = false;
2789
+ modelsCache = null;
2790
+ async ping() {
2791
+ try {
2792
+ const controller = new AbortController();
2793
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS6);
2794
+ try {
2795
+ const res = await fetch(`${GOOGLE_API_BASE}/models?key=${this.apiKey}`, {
2796
+ signal: controller.signal
2797
+ });
2798
+ return res.ok;
2799
+ } finally {
2800
+ clearTimeout(timer);
2801
+ }
2802
+ } catch {
2803
+ return false;
2804
+ }
2805
+ }
2806
+ async listModels(modality) {
2807
+ if (this.modelsCache) {
2808
+ return modality ? this.modelsCache.filter((m) => m.modality === modality) : this.modelsCache;
2809
+ }
2810
+ try {
2811
+ const controller = new AbortController();
2812
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS6);
2813
+ let data;
2814
+ try {
2815
+ const res = await fetch(`${GOOGLE_API_BASE}/models?key=${this.apiKey}`, {
2816
+ signal: controller.signal
2817
+ });
2818
+ if (!res.ok) return [];
2819
+ data = await res.json();
2820
+ } finally {
2821
+ clearTimeout(timer);
2822
+ }
2823
+ const entries = data?.models ?? [];
2824
+ const logo = getProviderLogo("google");
2825
+ const models = [];
2826
+ for (const entry of entries) {
2827
+ const modality2 = classifyGoogleModel(entry);
2828
+ if (!modality2) continue;
2829
+ const fullName = entry.name ?? "";
2830
+ const modelId = fullName.startsWith("models/") ? fullName.slice("models/".length) : fullName;
2831
+ const info = {
2832
+ id: modelId,
2833
+ provider: "google-media",
2834
+ name: entry.displayName ?? modelId,
2835
+ modality: modality2,
2836
+ local: false,
2837
+ cost: { price: 0, unit: modality2 === "video" ? "per_video" : "per_image" },
2838
+ logo,
2839
+ description: entry.description,
2840
+ capabilities: modality2 === "video" ? { maxDuration: 8, supportsStreaming: false } : modality2 === "tts" ? { voices: GOOGLE_TTS_VOICES } : void 0
2841
+ };
2842
+ models.push(info);
2843
+ }
2844
+ this.modelsCache = models;
2845
+ return modality ? models.filter((m) => m.modality === modality) : models;
2846
+ } catch {
2847
+ return [];
2848
+ }
2849
+ }
2850
+ async image(options) {
2851
+ const model = options.model ?? "imagen-4.0-generate-001";
2852
+ const start = Date.now();
2853
+ const body = {
2854
+ instances: [{ prompt: options.prompt }],
2855
+ parameters: {
2856
+ sampleCount: 1
2857
+ }
2858
+ };
2859
+ const res = await fetch(
2860
+ `${GOOGLE_API_BASE}/models/${model}:predict?key=${this.apiKey}`,
2861
+ {
2862
+ method: "POST",
2863
+ headers: { "Content-Type": "application/json" },
2864
+ body: JSON.stringify(body)
2865
+ }
2866
+ );
2867
+ if (!res.ok) {
2868
+ const errorBody = await res.text();
2869
+ throw new Error(`Google image generation failed (${res.status}): ${errorBody}`);
2870
+ }
2871
+ const data = await res.json();
2872
+ const base64 = data?.predictions?.[0]?.bytesBase64Encoded ?? data?.generatedImages?.[0]?.image?.imageBytes;
2873
+ if (!base64) {
2874
+ throw new Error("Google image generation returned no image data");
2875
+ }
2876
+ const buffer = Buffer.from(base64, "base64");
2877
+ return {
2878
+ buffer,
2879
+ provider: "google-media",
2880
+ model,
2881
+ modality: "image",
2882
+ latencyMs: Date.now() - start,
2883
+ usage: {
2884
+ cost: 0,
2885
+ unit: "per_image"
2886
+ },
2887
+ media: {
2888
+ format: "png"
2889
+ }
2890
+ };
2891
+ }
2892
+ async speak(options) {
2893
+ const model = options.model ?? "gemini-2.5-flash-preview-tts";
2894
+ const voice = options.voice ?? "Kore";
2895
+ const start = Date.now();
2896
+ const body = {
2897
+ contents: [{ parts: [{ text: options.text }] }],
2898
+ generationConfig: {
2899
+ response_modalities: ["AUDIO"],
2900
+ speech_config: {
2901
+ voiceConfig: {
2902
+ prebuiltVoiceConfig: { voiceName: voice }
2903
+ }
2904
+ }
2905
+ }
2906
+ };
2907
+ const res = await fetch(
2908
+ `${GOOGLE_API_BASE}/models/${model}:generateContent?key=${this.apiKey}`,
2909
+ {
2910
+ method: "POST",
2911
+ headers: { "Content-Type": "application/json" },
2912
+ body: JSON.stringify(body)
2913
+ }
2914
+ );
2915
+ if (!res.ok) {
2916
+ const errorBody = await res.text();
2917
+ throw new Error(`Google TTS failed (${res.status}): ${errorBody}`);
2918
+ }
2919
+ const data = await res.json();
2920
+ const inlineData = data?.candidates?.[0]?.content?.parts?.[0]?.inlineData;
2921
+ if (!inlineData?.data) {
2922
+ throw new Error("Google TTS returned no audio data");
2923
+ }
2924
+ const buffer = Buffer.from(inlineData.data, "base64");
2925
+ return {
2926
+ buffer,
2927
+ provider: "google-media",
2928
+ model,
2929
+ modality: "tts",
2930
+ latencyMs: Date.now() - start,
2931
+ usage: {
2932
+ cost: 0,
2933
+ input: options.text.length,
2934
+ unit: "per_1k_chars"
2935
+ },
2936
+ media: {
2937
+ format: "wav"
2938
+ // Google returns PCM L16, essentially WAV
2939
+ }
2940
+ };
2941
+ }
2942
+ async video(options) {
2943
+ const model = options.model ?? "veo-3.0-generate-001";
2944
+ const start = Date.now();
2945
+ const body = {
2946
+ instances: [{ prompt: options.prompt }],
2947
+ parameters: {
2948
+ sampleCount: 1
2949
+ }
2950
+ };
2951
+ if (options.duration) body.parameters.durationSeconds = options.duration;
2952
+ const res = await fetch(
2953
+ `${GOOGLE_API_BASE}/models/${model}:predictLongRunning?key=${this.apiKey}`,
2954
+ {
2955
+ method: "POST",
2956
+ headers: { "Content-Type": "application/json" },
2957
+ body: JSON.stringify(body)
2958
+ }
2959
+ );
2960
+ if (!res.ok) {
2961
+ const errorBody = await res.text();
2962
+ throw new Error(`Google video generation failed (${res.status}): ${errorBody}`);
2963
+ }
2964
+ const operation = await res.json();
2965
+ const operationName = operation?.name;
2966
+ if (!operationName) {
2967
+ throw new Error("Google video generation returned no operation name");
2968
+ }
2969
+ const deadline = Date.now() + 3e5;
2970
+ while (Date.now() < deadline) {
2971
+ await new Promise((r) => setTimeout(r, 5e3));
2972
+ const pollRes = await fetch(
2973
+ `${GOOGLE_API_BASE}/${operationName}?key=${this.apiKey}`
2974
+ );
2975
+ if (!pollRes.ok) continue;
2976
+ const status = await pollRes.json();
2977
+ if (status.done) {
2978
+ const videoBase64 = status.response?.generatedSamples?.[0]?.video?.bytesBase64Encoded;
2979
+ if (videoBase64) {
2980
+ return {
2981
+ buffer: Buffer.from(videoBase64, "base64"),
2982
+ provider: "google-media",
2983
+ model,
2984
+ modality: "video",
2985
+ latencyMs: Date.now() - start,
2986
+ usage: { cost: 0, unit: "per_video" },
2987
+ media: { format: "mp4", duration: options.duration }
2988
+ };
2989
+ }
2990
+ const videoUrl = status.response?.generatedSamples?.[0]?.video?.uri;
2991
+ return {
2992
+ url: videoUrl,
2993
+ provider: "google-media",
2994
+ model,
2995
+ modality: "video",
2996
+ latencyMs: Date.now() - start,
2997
+ usage: { cost: 0, unit: "per_video" },
2998
+ media: { format: "mp4", duration: options.duration }
2999
+ };
3000
+ }
3001
+ }
3002
+ throw new Error(`Google video generation timed out after 5 minutes`);
3003
+ }
3004
+ };
3005
+
2355
3006
  // src/noosphere.ts
2356
3007
  var Noosphere = class {
2357
3008
  config;
@@ -2534,9 +3185,9 @@ var Noosphere = class {
2534
3185
  if (!this.initialized) await this.init();
2535
3186
  return this.registry.getModel(provider, modelId);
2536
3187
  }
2537
- async syncModels() {
3188
+ async syncModels(modality) {
2538
3189
  if (!this.initialized) await this.init();
2539
- return this.registry.syncAll();
3190
+ return this.registry.syncAll(modality);
2540
3191
  }
2541
3192
  // --- Tracking Methods ---
2542
3193
  getUsage(options) {
@@ -2594,6 +3245,12 @@ var Noosphere = class {
2594
3245
  if (hasAnyLLMKey) {
2595
3246
  this.registry.addProvider(new PiAiProvider(llmKeys));
2596
3247
  }
3248
+ if (keys.openai) {
3249
+ this.registry.addProvider(new OpenAIMediaProvider(keys.openai));
3250
+ }
3251
+ if (keys.google) {
3252
+ this.registry.addProvider(new GoogleMediaProvider(keys.google));
3253
+ }
2597
3254
  if (keys.fal) {
2598
3255
  this.registry.addProvider(new FalProvider(keys.fal));
2599
3256
  }
@@ -2772,11 +3429,13 @@ var Noosphere = class {
2772
3429
  };
2773
3430
  export {
2774
3431
  AudioCraftProvider,
3432
+ GoogleMediaProvider,
2775
3433
  HfLocalProvider,
2776
3434
  Noosphere,
2777
3435
  NoosphereError,
2778
3436
  OllamaProvider,
2779
3437
  OpenAICompatProvider,
3438
+ OpenAIMediaProvider,
2780
3439
  PROVIDER_IDS,
2781
3440
  PROVIDER_LOGOS,
2782
3441
  WhisperLocalProvider,