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/README.md +95 -6
- package/dist/index.cjs +695 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -2
- package/dist/index.d.ts +34 -2
- package/dist/index.js +693 -34
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/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": "
|
|
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
|
|
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
|
-
|
|
1898
|
-
const modality = PIPELINE_TAG_TO_MODALITY[pipelineTag] ?? "image";
|
|
1899
|
-
models.push({
|
|
1988
|
+
entries.push({
|
|
1900
1989
|
id,
|
|
1901
|
-
|
|
1902
|
-
|
|
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
|
|
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,
|