Files
wehub-resource-sync adf0d17497
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:17:32 +08:00

350 lines
10 KiB
TypeScript

import type { PortType, Port } from "./workflow-types";
import { modalityForPort } from "./workflow-modalities";
import type { ModalityConfig } from "./workflow-modalities";
export function is_zero_gpu_space(s: any): boolean {
const tags: string[] = s?.cardData?.tags || s?.tags || [];
if (tags.includes("zero-gpu") || tags.includes("zerogpu")) return true;
const hwRaw = s?.runtime?.hardware ?? s?.runtime?.requestedHardware ?? "";
const hw =
typeof hwRaw === "string"
? hwRaw
: (hwRaw?.current ?? hwRaw?.requested ?? "");
return typeof hw === "string" && hw.toLowerCase().startsWith("zero");
}
export function normalize_space_id(raw: string): string | null {
const trimmed = raw.trim().replace(/\/+$/, "");
if (!trimmed) return null;
if (/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(trimmed)) {
return trimmed;
}
const pageMatch = trimmed.match(
/^https?:\/\/(?:www\.)?huggingface\.co\/(?:spaces|models|datasets)?\/?\/?([^/\s?#]+)\/([^/\s?#]+)/i
);
if (pageMatch) return `${pageMatch[1]}/${pageMatch[2]}`;
const sub = trimmed.match(/^https?:\/\/([^/]+)\.hf\.space/i);
if (sub) {
const parts = sub[1].split("-");
if (parts.length >= 2) {
return `${parts[0]}/${parts.slice(1).join("-")}`;
}
}
return null;
}
export function componentToPortType(
component: string,
type?: string,
pythonType?: string,
labelHint?: string
): PortType | "__skip__" {
const c = component.toLowerCase();
if (pythonType) {
const cleaned = pythonType
.toLowerCase()
.replace(/^none\s*\|\s*/, "")
.replace(/\s*\|\s*none$/, "")
.trim();
if (cleaned === "str" || cleaned === "string") return "text";
if (cleaned === "int" || cleaned === "integer" || cleaned === "float")
return "number";
if (cleaned === "bool" || cleaned === "boolean") return "boolean";
}
if (c === "image" || c === "imageeditor" || c === "imageslider")
return "image";
if (c === "gallery") return "gallery";
if (c === "audio") return "audio";
if (c === "video") return "video";
if (c === "number" || c === "slider") return "number";
if (c === "checkbox") return "boolean";
if (c === "file" || c === "uploadbutton" || c === "downloadbutton")
return "file";
if (c === "model3d") return "model3d";
if (c === "json" || c === "dataframe") return "json";
if (c === "state") return "__skip__";
if (
c === "textbox" ||
c === "text" ||
c === "markdown" ||
c === "chatbot" ||
c === "label" ||
c === "code" ||
c === "highlightedtext" ||
c === "dropdown" ||
c === "radio" ||
c === "checkboxgroup" ||
c === "colorpicker" ||
c === "html"
)
return "text";
// gr.api endpoints use component="Api" with python_type carrying the
// real hint — primitives already handled at the top of the function.
if (pythonType) {
const cleaned = pythonType
.toLowerCase()
.replace(/^none\s*\|\s*/, "")
.replace(/\s*\|\s*none$/, "")
.trim();
if (
cleaned === "filepath" ||
cleaned === "file" ||
cleaned.includes("filedata")
) {
const l = (labelHint ?? "").toLowerCase();
if (/audio|wav|mp3|voice|sound|speech|tts|asr/.test(l)) return "audio";
if (/image|img|photo|picture|pic\b/.test(l)) return "image";
if (/video|mp4|movie|clip/.test(l)) return "video";
if (/model3d|mesh|glb|gltf|obj\b/.test(l)) return "model3d";
return "file";
}
if (
cleaned.includes("dict") ||
cleaned.includes("list") ||
cleaned.includes("any")
)
return "json";
}
// Fallback: check the type field from the API
if (type) {
const t = typeof type === "string" ? type.toLowerCase() : "";
if (t.includes("image") || t.includes("pil")) return "image";
if (t.includes("video") || t.includes("mp4")) return "video";
if (t.includes("audio") || t.includes("wav") || t.includes("mp3"))
return "audio";
if (t.includes("filepath") || t.includes("file")) return "file";
if (t === "number" || t === "float" || t === "int" || t === "integer")
return "number";
if (t === "bool" || t === "boolean") return "boolean";
if (t === "str" || t === "string") return "text";
}
return "any";
}
/** Prefixes that indicate utility/event-handler endpoints — skip these */
const UTILITY_PREFIXES = [
"/on_",
"/handle_",
"/update_",
"/prepare_",
"/load_",
"/clear_",
"/reset_"
];
export interface EndpointSig {
name: string;
inputs: Port[];
outputs: Port[];
}
export interface SpaceApiInfo {
endpoint: string;
inputs: Port[];
outputs: Port[];
width: number;
endpoints: EndpointSig[];
}
const ORDINAL_LABEL = /^\d+(st|nd|rd|th)$/i;
export function extract_choices(
type: unknown
): { choices: string[]; multiselect: boolean } | null {
if (!type || typeof type !== "object") return null;
const t = type as { type?: string; enum?: unknown[]; items?: any };
if (Array.isArray(t.enum) && t.enum.length > 0) {
return { choices: t.enum.map(String), multiselect: false };
}
if (
t.type === "array" &&
t.items &&
Array.isArray(t.items.enum) &&
t.items.enum.length > 0
) {
return { choices: t.items.enum.map(String), multiselect: true };
}
return null;
}
function parse_endpoint_sig(name: string, ep: any): EndpointSig {
const inputs = (ep.parameters ?? [])
.map((p: any, i: number) => {
const isApi = (p.component ?? "").toLowerCase() === "api";
const pyType = p.python_type?.type;
const labelHint = p.parameter_name || p.label || "";
const portType = componentToPortType(
p.component ?? "",
typeof p.type === "string" ? p.type : "",
pyType,
labelHint
);
if (portType === "__skip__") return null;
const hasDefault = p.parameter_has_default === true;
const useParamName =
(isApi || ORDINAL_LABEL.test(p.label ?? "")) && p.parameter_name;
const rawLabel = useParamName
? p.parameter_name
: p.label || p.parameter_name || `Input ${i}`;
const choiceInfo = extract_choices(p.type);
return {
id: `in_${i}`,
label: rawLabel,
type: portType,
required: !hasDefault,
default_value:
hasDefault && p.default !== undefined ? p.default : undefined,
...(choiceInfo ?? {})
};
})
.filter(Boolean) as Port[];
const outputs = (ep.returns ?? [])
.map((r: any, i: number) => {
const isApi = (r.component ?? "").toLowerCase() === "api";
const pyType = r.python_type?.type;
const labelHint = isApi
? `${r.parameter_name ?? ""} ${name ?? ""}`.trim()
: r.parameter_name || r.label || "";
const portType = componentToPortType(
r.component ?? "",
typeof r.type === "string" ? r.type : "",
pyType,
labelHint
);
if (portType === "__skip__") return null;
const useParamName =
(isApi || ORDINAL_LABEL.test(r.label ?? "")) && r.parameter_name;
const rawLabel = useParamName
? r.parameter_name
: r.label || `Output ${i}`;
return {
id: `out_${i}`,
label: rawLabel,
type: portType,
output_index: i
};
})
.filter(Boolean) as Port[];
if (inputs.length === 0)
inputs.push({ id: "in", label: "Input", type: "any" as PortType });
if (outputs.length === 0)
outputs.push({ id: "out", label: "Output", type: "any" as PortType });
return { name, inputs, outputs };
}
/** Cache for fetched API info */
const spaceApiCache = new Map<string, SpaceApiInfo>();
/**
* Fetch API info from a Space and return endpoint, inputs, outputs.
* Results are cached by space_id.
*/
export async function fetchSpaceApi(spaceId: string): Promise<SpaceApiInfo> {
const cached = spaceApiCache.get(spaceId);
if (cached) return cached;
const spaceUrl = spaceId.startsWith("http")
? spaceId
: `https://${spaceId.replace("/", "-").replaceAll(".", "-").toLowerCase()}.hf.space`;
// Try multiple API paths — older Spaces use /info, newer use /gradio_api/info
let infoRes: Response | null = null;
for (const path of ["/gradio_api/info", "/info", "/api/info"]) {
try {
const res = await Promise.race([
fetch(`${spaceUrl}${path}`),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("Timed out")), 10000)
)
]);
if (res.ok) {
infoRes = res;
break;
}
} catch {
/* try next */
}
}
if (!infoRes)
throw new Error(
"Could not connect to Space — it may be sleeping or have no Gradio API"
);
const api = await infoRes.json();
const named = api.named_endpoints ?? {};
const unnamed = api.unnamed_endpoints ?? {};
const all_endpoints: [string, any][] = [
...Object.entries(named),
...Object.entries(unnamed)
].filter(([n]) => !UTILITY_PREFIXES.some((p) => n.startsWith(p)));
if (all_endpoints.length === 0) {
throw new Error("No suitable endpoints found");
}
// Default to /predict if present (most common Gradio convention), else
// the first non-utility endpoint. Users can switch via the node dropdown.
const default_index = Math.max(
0,
all_endpoints.findIndex(([n]) => n === "/predict")
);
const [epName, ep] = all_endpoints[default_index];
const endpoints: EndpointSig[] = all_endpoints.map(([n, e]) =>
parse_endpoint_sig(n, e)
);
const picked = parse_endpoint_sig(epName, ep);
const inputs = picked.inputs;
const outputs = picked.outputs;
const label = spaceId.split("/").pop() ?? spaceId;
const maxPortLen = Math.max(
...inputs.map((p: any) => (p.label as string).length),
...outputs.map((p: any) => (p.label as string).length),
label.length
);
const width = Math.max(280, Math.min(400, maxPortLen * 9 + 100));
const result = { endpoint: epName, inputs, outputs, width, endpoints };
spaceApiCache.set(spaceId, result);
return result;
}
export function normalizeOperatorPorts(
modality: ModalityConfig | null,
ports: Port[],
types?: PortType[]
): Port[] {
const fallback = modality?.port_type ?? null;
if (!fallback && (!types || types.length === 0)) return ports;
return ports.map((p, i) => {
if (p.type !== "any" && p.type !== "file") return p;
const hint = types?.[i] ?? fallback;
if (!hint) return p;
return { ...p, type: hint };
});
}
/**
* Canonicalize a single port type using the modality registry. Used by
* schema lookups (TASK_SCHEMAS) to clamp legacy `any`/`file` entries.
* Pass-through for already-specific types.
*/
export function canonicalizePort(type: PortType, hint?: PortType): PortType {
if (type !== "any" && type !== "file") return type;
if (!hint) return type;
const modality = modalityForPort(hint);
return modality?.port_type ?? type;
}