chore: import upstream snapshot with attribution
publish / version_or_publish (push) Waiting to run
storybook-build / changes (push) Waiting to run
storybook-build / :storybook-build (push) Blocked by required conditions
Sync Gradio Skills to Hugging Face / sync-skills (push) Waiting to run
functional / changes (push) Waiting to run
functional / build-frontend (push) Blocked by required conditions
functional / functional-test-SSR=false (push) Blocked by required conditions
functional / functional-reload (push) Blocked by required conditions
functional / functional-test-SSR=true (push) Blocked by required conditions
hygiene / hygiene-test (push) Waiting to run
python / changes (push) Waiting to run
python / build (push) Blocked by required conditions
python / test-ubuntu-latest-flaky (push) Blocked by required conditions
python / test-ubuntu-latest-not-flaky (push) Blocked by required conditions
python / test-windows-latest-flaky (push) Blocked by required conditions
python / test-windows-latest-not-flaky (push) Blocked by required conditions
js / changes (push) Waiting to run
js / js-test (push) Blocked by required conditions
docs-build / changes (push) Waiting to run
docs-build / docs-build (push) Blocked by required conditions
docs-build / website-build (push) Blocked by required conditions
publish / version_or_publish (push) Waiting to run
storybook-build / changes (push) Waiting to run
storybook-build / :storybook-build (push) Blocked by required conditions
Sync Gradio Skills to Hugging Face / sync-skills (push) Waiting to run
functional / changes (push) Waiting to run
functional / build-frontend (push) Blocked by required conditions
functional / functional-test-SSR=false (push) Blocked by required conditions
functional / functional-reload (push) Blocked by required conditions
functional / functional-test-SSR=true (push) Blocked by required conditions
hygiene / hygiene-test (push) Waiting to run
python / changes (push) Waiting to run
python / build (push) Blocked by required conditions
python / test-ubuntu-latest-flaky (push) Blocked by required conditions
python / test-ubuntu-latest-not-flaky (push) Blocked by required conditions
python / test-windows-latest-flaky (push) Blocked by required conditions
python / test-windows-latest-not-flaky (push) Blocked by required conditions
js / changes (push) Waiting to run
js / js-test (push) Blocked by required conditions
docs-build / changes (push) Waiting to run
docs-build / docs-build (push) Blocked by required conditions
docs-build / website-build (push) Blocked by required conditions
This commit is contained in:
@@ -0,0 +1,381 @@
|
||||
---
|
||||
name: "frontend-unit-testing"
|
||||
description: "Write comprehensive, behaviour-driven unit tests for Gradio frontend Svelte components using Vitest browser mode, Playwright, and the @self/tootils test utilities."
|
||||
---
|
||||
|
||||
# Frontend Unit Testing Skill
|
||||
|
||||
You are an expert at writing unit tests for Gradio's Svelte frontend components. Follow these instructions precisely.
|
||||
|
||||
## Core Principles (Non-Negotiable)
|
||||
|
||||
1. **Test everything.** Unit tests are cheap. Having too many is a problem we want to have. When in doubt, write the test. Multiple tests per feature/argument is fine and encouraged.
|
||||
|
||||
2. **Test behaviour, not implementation.** Never assert on implementation details like CSS class names, internal state, or DOM structure for its own sake. Instead, test observable behaviour.
|
||||
- BAD: assert that an input has a `step` attribute set to `5`
|
||||
- GOOD: type a value, click the increment button, and assert the value increased by `5`
|
||||
- BAD: assert that a container has class `hidden`
|
||||
- GOOD: assert that the element is not visible with `toBeVisible()`
|
||||
|
||||
3. **Test Gradio-specific functionality.** Every component has `get_data`, `set_data`, and dispatches events. These must be tested, including their interactions with props.
|
||||
- `set_data` -> verify the DOM reflects the change
|
||||
- `get_data` -> verify it returns the current state
|
||||
- `set_data` -> `get_data` round-trips
|
||||
- User interaction -> `get_data` reflects it
|
||||
- Events: `change`, `input`, `submit`, `blur`, `focus`, `clear`, `upload`, `select`, `custom_button_click`, etc.
|
||||
|
||||
4. **Real browser environment.** Tests run in Vitest browser mode with a Playwright provider. This is a real browser, not jsdom. **Do not mock or stub** unless absolutely unavoidable (e.g., `navigator.clipboard`, `MediaStream`). If you must mock, explain why in a comment.
|
||||
|
||||
5. **Test sub-components in isolation** when they have meaningful standalone logic (e.g., a utility function, a shared inner component). These tests are *in addition to* full `Index.svelte` integration tests.
|
||||
|
||||
6. **Never refactor production code for testability** without explicit user approval. If a refactor would help, recommend it and wait for a go-ahead.
|
||||
|
||||
7. **Visual-only props get `test.todo` placeholders.** If a prop or argument results in a purely visual change (colours, spacing, fonts, border styles, shadows, etc.) that cannot be meaningfully asserted with behavioural queries, do NOT skip it silently. Instead:
|
||||
- Add a `test.todo("description")` explaining that it needs a visual regression test
|
||||
- The description should state what prop/value is being tested and what the expected visual outcome is
|
||||
- This ensures visual-only behaviour is tracked and not forgotten
|
||||
|
||||
```ts
|
||||
test.todo(
|
||||
"VISUAL: container_color='red' applies a red background to the component wrapper — needs Playwright visual regression screenshot comparison"
|
||||
);
|
||||
```
|
||||
|
||||
8. **No useless comments.** Comments should be used exceptionally, only when clarification of the code is essential. Do NOT create comments to describe types of tests (`describe` blocks do that). Do NOT add comments explaining the flow of the code (the code does that). Only add comments when something is confusing or complex, adds useful context (i.e. giving more detail on the failure case it is guarding against), or goes against our principles (this requires a comment + rationale).
|
||||
|
||||
## Test Environment & Utilities
|
||||
|
||||
All test utilities come from `@self/tootils/render`. Never import from `@testing-library/svelte` directly.
|
||||
|
||||
### `render(Component, props?, options?)`
|
||||
|
||||
Mounts a Gradio component with the full shared prop infrastructure (loading_status, dispatcher, i18n, etc.).
|
||||
|
||||
Returns:
|
||||
- `container` — the root DOM element
|
||||
- `listen(event_name, opts?)` — returns a `vi.fn()` mock that records dispatched Gradio events. By default only captures events fired *after* the call. **Use `{ retrospective: true }` when testing mount-time events** — this replays any events that were buffered during render before `listen` was called. Without this flag, mount-time events are invisible.
|
||||
- `set_data(data)` — programmatically set component data (simulates backend push). Automatically ticks.
|
||||
- `get_data()` — read current component data.
|
||||
- All `@testing-library/dom` query functions (`getByRole`, `getByText`, `getByDisplayValue`, `queryByRole`, etc.)
|
||||
- `debug()` — prints pretty DOM to console.
|
||||
- `unmount()` — teardown.
|
||||
|
||||
Props are split automatically: keys in `allowed_shared_props` go to `shared_props`, everything else goes to `props`.
|
||||
|
||||
### `fireEvent`
|
||||
|
||||
Re-exported from `@testing-library/dom` but wrapped to `await tick()` twice after each event (to let Svelte reactivity settle). Always `await` fireEvent calls.
|
||||
|
||||
### `cleanup()`
|
||||
|
||||
Call in `afterEach` to unmount all rendered components.
|
||||
|
||||
### `run_shared_prop_tests(config)` (MANDATORY)
|
||||
|
||||
Runs a standard suite of shared prop tests (`elem_id`, `elem_classes`, `visible`, `label`, `show_label`, `validation_error`). **Every component test file MUST call this.** Never manually re-implement these tests.
|
||||
|
||||
```ts
|
||||
run_shared_prop_tests({
|
||||
component: MyComponent,
|
||||
name: "MyComponent",
|
||||
base_props: { /* minimum props to render */ },
|
||||
has_label: true, // default true; false for label-less components
|
||||
has_validation_error: true // default true
|
||||
});
|
||||
```
|
||||
|
||||
**When a shared test doesn't apply to a component** (e.g., the component has no label), use the config flags to disable that specific test — do NOT skip `run_shared_prop_tests` entirely and rewrite everything by hand:
|
||||
|
||||
```ts
|
||||
// Accordion has no label — disable label tests, keep everything else
|
||||
run_shared_prop_tests({
|
||||
component: Accordion,
|
||||
name: "Accordion",
|
||||
base_props: { label: "Section", open: true },
|
||||
has_label: false,
|
||||
has_validation_error: false
|
||||
});
|
||||
```
|
||||
|
||||
If a shared test fails for a component-specific reason that the flags don't cover, the correct response is to:
|
||||
1. Still call `run_shared_prop_tests` with appropriate flags to cover what it can
|
||||
2. Write a targeted custom test for the specific behaviour that differs
|
||||
3. Explain why the shared test doesn't apply in a comment
|
||||
|
||||
### File utilities
|
||||
|
||||
- `upload_file(fixture, selector?)` — sets files on a file input using real fixtures
|
||||
- `drop_file(fixture, selector)` — simulates drag-and-drop with real files
|
||||
- `download_file(selector)` — clicks an element and captures the download
|
||||
- `mock_client()` — returns a mock client for components that use file uploads (the upload mock echoes input unchanged)
|
||||
|
||||
### Fixtures
|
||||
|
||||
Pre-built `FileData` objects pointing to real test files:
|
||||
- `TEST_TXT`, `TEST_JPG`, `TEST_PNG`, `TEST_MP4`, `TEST_WAV`, `TEST_PDF`
|
||||
|
||||
### User events
|
||||
|
||||
For keyboard/typing interactions, import `@testing-library/user-event`:
|
||||
|
||||
```ts
|
||||
import event from "@testing-library/user-event";
|
||||
|
||||
el.focus();
|
||||
await event.keyboard("some text");
|
||||
await event.type(el, "123");
|
||||
await event.clear(el);
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
**Always use `pnpm test:run`.** Never use `pnpm test` — it starts in watch mode and never exits.
|
||||
|
||||
**Always prefix test commands with `CI=1`** so Vitest runs Playwright in headless mode (`headless: !!process.env.CI` in `js/spa/vite.config.ts`). This prevents a visible Chromium window from opening on the developer's desktop during agent verification runs.
|
||||
|
||||
All commands are run from the repo root.
|
||||
|
||||
```bash
|
||||
# Run all unit tests (headless)
|
||||
CI=1 pnpm test:run
|
||||
|
||||
# Run a specific test file (match by filename)
|
||||
CI=1 pnpm test:run Textbox.test.ts
|
||||
|
||||
# Run all tests within a folder (match by path segment)
|
||||
CI=1 pnpm test:run dataframe
|
||||
|
||||
# Filter by test name with -t
|
||||
CI=1 pnpm test:run -t elem_id
|
||||
|
||||
# Combine file/folder filter with test name filter
|
||||
CI=1 pnpm test:run accordion -t elem_id
|
||||
|
||||
# Single file via vitest directly
|
||||
CI=1 npx vitest run --config .config/vitest.config.ts js/<pkg>/<Test>.test.ts
|
||||
```
|
||||
|
||||
If tests fail with a missing `chrome-headless-shell` executable, install it once:
|
||||
|
||||
```bash
|
||||
pnpm exec playwright install chromium --only-shell
|
||||
```
|
||||
|
||||
After writing or modifying tests, always run them to verify they pass.
|
||||
|
||||
**Do NOT run browser tests automatically on branch checkout or `git pull`.** Only run tests when the user explicitly asks, or immediately after writing/modifying a test file. Branch operations are routine and should not trigger test runs.
|
||||
|
||||
## Test File Structure
|
||||
|
||||
```ts
|
||||
import { test, describe, afterEach, expect, vi } from "vitest";
|
||||
import { cleanup, render, fireEvent, waitFor } from "@self/tootils/render";
|
||||
import { run_shared_prop_tests } from "@self/tootils/shared-prop-tests";
|
||||
import event from "@testing-library/user-event";
|
||||
|
||||
import Component from "./Index.svelte";
|
||||
|
||||
const default_props = {
|
||||
// Minimum props for a working render, always including:
|
||||
label: "Component Name",
|
||||
show_label: true,
|
||||
interactive: true,
|
||||
// ...component-specific props
|
||||
};
|
||||
|
||||
// 1. Shared prop tests
|
||||
run_shared_prop_tests({
|
||||
component: Component,
|
||||
name: "ComponentName",
|
||||
base_props: { /* ... */ }
|
||||
});
|
||||
|
||||
// 2. Describe blocks grouped by prop, feature, or concern
|
||||
describe("ComponentName", () => {
|
||||
afterEach(() => cleanup());
|
||||
// General rendering and basic behaviour
|
||||
});
|
||||
|
||||
describe("Props: propName", () => {
|
||||
afterEach(() => cleanup());
|
||||
// Tests for each meaningful prop value
|
||||
});
|
||||
|
||||
describe("Events", () => {
|
||||
afterEach(() => cleanup());
|
||||
// change, input, submit, blur, focus, clear, etc.
|
||||
});
|
||||
|
||||
describe("get_data / set_data", () => {
|
||||
afterEach(() => cleanup());
|
||||
// Round-trip, DOM reflection, interaction flow
|
||||
});
|
||||
|
||||
describe("Edge cases", () => {
|
||||
afterEach(() => cleanup());
|
||||
// Null/undefined handling, deduplication, mount-time behaviour
|
||||
});
|
||||
```
|
||||
|
||||
### Naming conventions
|
||||
|
||||
- **Describe blocks**: Group by `Props: <name>`, `Events`, `Events: <name>`, `get_data / set_data`, `Edge cases`, or component area.
|
||||
- **Test names**: Declarative sentences describing what should happen. e.g., `"lines > 1 renders a textarea with correct rows"`, `"change: emitted when value changes from outside"`.
|
||||
|
||||
## Two Modes of Operation
|
||||
|
||||
### Mode 1: Targeted tests (for a specific feature or regression)
|
||||
|
||||
When asked to write tests for a specific feature, prop, or bug fix:
|
||||
- Read the relevant component source to understand the behaviour
|
||||
- Write focused tests covering the specific area
|
||||
- Include edge cases related to that feature
|
||||
- Proceed directly — no plan needed unless the scope is ambiguous
|
||||
|
||||
### Mode 2: Full component test battery
|
||||
|
||||
When asked to write or rewrite tests for an entire component:
|
||||
|
||||
**You MUST follow this process:**
|
||||
|
||||
1. **Research phase** — Read thoroughly:
|
||||
- The component's `Index.svelte` (the main entry point)
|
||||
- Any shared sub-components in the component's `shared/` directory
|
||||
- The Python component definition in `gradio/components/` to understand all props, events, and data types
|
||||
- Any existing tests for the component
|
||||
- The component's demo files in `demo/` if they exist
|
||||
|
||||
2. **Analysis phase** — Identify every testable surface:
|
||||
- Every prop and its meaningful values (including defaults, edge cases, combinations)
|
||||
- Every event the component dispatches
|
||||
- `get_data` / `set_data` behaviour
|
||||
- Interactive vs non-interactive behaviour
|
||||
- Sub-components with standalone testable logic
|
||||
- Accessibility-relevant behaviour (labels, ARIA attributes as they affect user behaviour)
|
||||
- Edge cases: null/undefined values, empty strings, boundary values, mount-time behaviour, event deduplication
|
||||
- Visual-only props: identify props that only affect appearance and flag them for `test.todo` with visual regression notes
|
||||
|
||||
3. **Plan phase** — Present a structured testing plan:
|
||||
- Organised by describe block
|
||||
- Each test listed with: name, what it verifies, key assertion
|
||||
- Call out any tests that might need mocking (and why)
|
||||
- Call out any sub-components worth testing in isolation
|
||||
- Call out any visual-only props that need `test.todo` placeholders for visual regression testing
|
||||
- Note any refactoring that would improve testability (but don't do it)
|
||||
|
||||
4. **Wait for approval** — Present the plan and ask for feedback before writing code.
|
||||
|
||||
5. **Implementation phase** — Write the tests following the plan.
|
||||
|
||||
## Assertion Patterns
|
||||
|
||||
### Query priority (strict)
|
||||
|
||||
Always use the query utilities returned by `render()`. **Never use `container.querySelector` unless every option below has been exhausted.** Follow this priority order:
|
||||
|
||||
1. **Semantic role queries** (best — reflects how users and assistive tech see the component):
|
||||
```ts
|
||||
getByRole("textbox")
|
||||
getByRole("button", { name: "Submit" })
|
||||
getByRole("slider")
|
||||
```
|
||||
|
||||
2. **Label and text queries** (good — reflects visible content):
|
||||
```ts
|
||||
getByLabelText("Upload file")
|
||||
getByText("Submit")
|
||||
getByDisplayValue("hello")
|
||||
getByPlaceholderText("Enter text...")
|
||||
```
|
||||
|
||||
3. **Test ID queries** (required fallback — when no semantic/text query works):
|
||||
```ts
|
||||
getByTestId("source-select")
|
||||
getByTestId("password")
|
||||
```
|
||||
If the element lacks a `data-testid`, **add one to the component source**. This is always the right move. `container.querySelector` is never acceptable — adding a `data-testid` is cheap, explicit, and keeps test intent clear.
|
||||
|
||||
Use `queryBy*` variants (which return `null` instead of throwing) when asserting something is **not** in the DOM:
|
||||
```ts
|
||||
expect(queryByRole("button")).not.toBeInTheDocument();
|
||||
expect(queryByLabelText("Upload file")).not.toBeInTheDocument();
|
||||
```
|
||||
|
||||
### Common assertion patterns
|
||||
|
||||
```ts
|
||||
// Visibility
|
||||
expect(el).toBeVisible();
|
||||
expect(el).not.toBeVisible();
|
||||
|
||||
// Presence
|
||||
expect(queryByRole("button")).not.toBeInTheDocument(); // not in DOM
|
||||
expect(getByRole("button")).toBeInTheDocument(); // in DOM
|
||||
|
||||
// Values
|
||||
expect(el).toHaveValue("hello");
|
||||
expect(el).toHaveAttribute("type", "password");
|
||||
|
||||
// State
|
||||
expect(el).toBeDisabled();
|
||||
expect(el).toBeEnabled();
|
||||
expect(el).toHaveFocus();
|
||||
|
||||
// Events
|
||||
const change = listen("change");
|
||||
await set_data({ value: "new" });
|
||||
expect(change).toHaveBeenCalledTimes(1);
|
||||
expect(change).toHaveBeenCalledWith("new");
|
||||
|
||||
// Mount-time events — use { retrospective: true }
|
||||
// listen() only captures events fired AFTER it is called. Since render()
|
||||
// is awaited before listen() runs, any events fired during mount are missed.
|
||||
// Pass { retrospective: true } to also replay events from the buffer.
|
||||
//
|
||||
// Use this whenever you need to assert about mount-time behaviour:
|
||||
// - "no spurious change event on mount"
|
||||
// - "component fires 'load' on mount"
|
||||
// - "initial value triggers change on mount" (or doesn't)
|
||||
const change = listen("change", { retrospective: true });
|
||||
expect(change).not.toHaveBeenCalled(); // no spurious mount event
|
||||
|
||||
// If you expect an event WAS fired on mount:
|
||||
const load = listen("load", { retrospective: true });
|
||||
expect(load).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Event deduplication
|
||||
await set_data({ value: "x" });
|
||||
await set_data({ value: "x" });
|
||||
expect(change).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Async operations (uploads, etc.)
|
||||
await waitFor(() => {
|
||||
expect(upload).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
```
|
||||
|
||||
## What NOT to Do
|
||||
|
||||
- **Never use `container.querySelector`**. It is unconditionally banned. Use `getByRole`, `getByText`, `getByLabelText`, `getByDisplayValue`, `getByPlaceholderText`, or `getByTestId`. If none of those work, add a `data-testid` attribute to the component source — this is always the correct solution.
|
||||
- **Don't mock Svelte internals**, the DOM, or browser APIs that work natively.
|
||||
- **Don't unit-test purely visual styling** (colours, spacing, fonts, shadows). Instead, add `test.todo` placeholders recommending Playwright visual regression tests. Do test behavioural *effects* of styling (visibility, disabled state).
|
||||
- **Don't assert on internal class names** unless they are the mechanism by which a behaviour is expressed and there's no semantic alternative (e.g., `.sr-only` for screen-reader-only labels).
|
||||
- **Don't manually rewrite shared prop tests.** `run_shared_prop_tests` handles `elem_id`, `elem_classes`, `visible`, `label`, `show_label`, `validation_error`. Always call it. If a specific test doesn't apply, use the config flags (`has_label: false`, `has_validation_error: false`) — never skip the utility and hand-roll the tests instead.
|
||||
- **Don't use `toBeTruthy()` or `toBeFalsy()`.** These are too vague and hide intent. Use the most specific matcher for the value being checked:
|
||||
- Element in DOM → `toBeInTheDocument()` / `.not.toBeInTheDocument()`
|
||||
- Element visible → `toBeVisible()` / `.not.toBeVisible()`
|
||||
- Element has value → `toHaveValue("x")`
|
||||
- Element checked → `toBeChecked()`
|
||||
- Element disabled → `toBeDisabled()` / `toBeEnabled()`
|
||||
- Boolean variable → `toBe(true)` / `toBe(false)`
|
||||
- Array non-empty → `toHaveLength(n)` or `expect(arr.length).toBeGreaterThan(0)`
|
||||
- Non-DOM null/undefined → `toBeNull()` / `toBeDefined()` / `toBeUndefined()`
|
||||
- **Don't add `setTimeout` or artificial delays.** Use `await tick()`, `await fireEvent.x()`, or `await waitFor()`.
|
||||
- **Don't write snapshot tests.** They test implementation, not behaviour.
|
||||
- **Don't import from `@testing-library/svelte`** — always use `@self/tootils/render`.
|
||||
|
||||
## Reference: Exemplar Test Files
|
||||
|
||||
Study these files for patterns and quality bar:
|
||||
- `js/textbox/Textbox.test.ts` — comprehensive prop, event, and edge case testing
|
||||
- `js/image/Image.test.ts` — file upload/drop, sub-component isolation (`get_coordinates_of_clicked_image`), interactive vs static modes, custom buttons
|
||||
@@ -0,0 +1,266 @@
|
||||
---
|
||||
name: gradio-themes
|
||||
description: Build and customise Gradio themes. Use when creating, editing, or publishing Python-based Gradio themes that control colours, typography, spacing, shadows, and dark mode.
|
||||
---
|
||||
|
||||
# Gradio Theme Building
|
||||
|
||||
You are an expert at building Gradio themes. Themes control the entire visual identity of a Gradio app — colours, typography, spacing, shadows, and dark mode — through Python classes that compile to CSS custom properties.
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **Text contrast is non-negotiable.** Every text element must be readable against its actual background — body text, label text on coloured label fills, button text on button fills, placeholder text, selected checkbox text, error text, link text. A beautiful theme that can't be read is useless. Audit every text/background pair before shipping.
|
||||
|
||||
2. **Dark mode is independently designed.** Never auto-invert light values. Every `_dark` variable should be intentionally chosen for contrast and readability on dark backgrounds. Specifically:
|
||||
- Reduce font weight slightly on dark (350 instead of 400) — light text on dark reads heavier
|
||||
- Desaturate accents — high chroma at high lightness looks garish
|
||||
- Use lighter surfaces for elevation, not heavier shadows
|
||||
- Never use pure black `#000` — use `#0a0a14`-ish dark with a subtle hue cast
|
||||
|
||||
3. **Commit to an aesthetic direction.** Bold maximalism and refined minimalism both work — what fails is half-commitment. Pick one tone (editorial, brutal, glass, retro, organic, playful, industrial...) and execute every variable in service of it.
|
||||
|
||||
4. **Use variable references (`*name`) for consistency.** When one value should track another, reference it. This keeps themes maintainable and lets users customise constructor params (hues, sizes) and have changes cascade.
|
||||
|
||||
5. **Test both modes.** Always verify light AND dark for body, blocks, inputs, buttons, labels, checkboxes, tables, focus states, hover states, selected states.
|
||||
|
||||
## Aesthetic Quality
|
||||
|
||||
Technical correctness isn't enough. A theme can have every variable set perfectly and still look generic. Apply these checks:
|
||||
|
||||
### The "AI Slop" Test
|
||||
If you showed this theme to someone and said "AI made this," would they immediately believe you? If yes, that's the problem. A distinctive theme makes someone ask *"how was this made?"* not *"which AI made this?"*
|
||||
|
||||
### Palette traps to avoid
|
||||
- **Cyan accents on near-black backgrounds** — the default "AI cyberpunk" look
|
||||
- **Purple-to-blue gradients** — overused and dated
|
||||
- **Neon glow on dark mode** — looks "cool" without requiring real design decisions
|
||||
- **Gradient text on headings/metrics** — decorative, not meaningful
|
||||
- **Glassmorphism everywhere** — backdrop-blur as decoration rather than purpose
|
||||
- **Pure black (`#000`) or pure white (`#fff`)** — don't exist in nature; tint everything (even chroma 0.005-0.01 reads as natural)
|
||||
- **Untinted neutrals** (`colors.gray`, `colors.zinc` straight) — neutrals should hint at the brand hue for subconscious cohesion. Use `colors.slate` if your accent is cool, `colors.stone` if warm.
|
||||
- **Heavy alpha use** (`rgba(...)` everywhere) — usually means an incomplete palette. Define explicit overlay colours per context. Acceptable for focus rings and frosted glass; suspect everywhere else.
|
||||
- **Gray text on coloured backgrounds** — washes out. Use a darker shade of the background colour instead.
|
||||
|
||||
### Typography traps to avoid
|
||||
- **Inter, Roboto, Open Sans, Lato, Montserrat** — these are the invisible defaults that signal "AI-generated." Fine for utility themes; lethal for distinctive ones.
|
||||
- **Better Google Font alternatives**: Instrument Sans, Plus Jakarta Sans, Outfit, Onest, Figtree, DM Sans, Source Sans 3 (sans); Fraunces, Newsreader, Lora (serif/editorial); Chakra Petch, Space Grotesk, JetBrains Mono (technical)
|
||||
- **Monospace as lazy "technical" shorthand** — only use mono when it actually communicates something
|
||||
- **Too many font sizes too close together** (12, 13, 14, 15, 16) — creates muddy hierarchy. Use fewer sizes with more contrast (1.25–1.5× ratio).
|
||||
|
||||
### Visual detail traps to avoid
|
||||
- **Generic drop shadows** (`0 2px 4px rgba(0,0,0,0.1)`) — safe, forgettable. If you can clearly see the shadow, it's too strong. Either commit to bold shadows or none.
|
||||
- **Identical card grids** — every block looking the same shape and weight creates visual monotony.
|
||||
- **Uniform spacing** — vary tight groupings with generous separations to create rhythm.
|
||||
|
||||
### Hierarchy through multiple dimensions
|
||||
Hierarchy is strongest when 2–3 of {size, weight, colour, position, space} change at once. A bigger label alone is weak; bigger + bolder + more space above is strong. Apply this to `block_label_*` vs `block_title_*` vs `section_header_*`.
|
||||
|
||||
## Architecture
|
||||
|
||||
Themes inherit from `gradio.themes.Base`, which defines 300+ CSS variables with defaults. Your theme overrides specific variables via `super().set()`.
|
||||
|
||||
**Flow:** Python class → `_get_theme_css()` → CSS `:root { --var: val; }` → served at `/theme.css` → consumed by Svelte components via `var(--name)`.
|
||||
|
||||
Theme CSS is injected into the `<gradio-app>` shadow DOM. The page's `<body>` is styled separately via `body_background_fill` (applied as `body { background: var(--body-background-fill) }` in the layout).
|
||||
|
||||
## Skeleton
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
from collections.abc import Iterable
|
||||
|
||||
from gradio.themes.base import Base
|
||||
from gradio.themes.utils import colors, fonts, sizes
|
||||
|
||||
|
||||
class MyTheme(Base):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
primary_hue: colors.Color | str = colors.blue,
|
||||
secondary_hue: colors.Color | str = colors.violet,
|
||||
neutral_hue: colors.Color | str = colors.slate,
|
||||
spacing_size: sizes.Size | str = sizes.spacing_md,
|
||||
radius_size: sizes.Size | str = sizes.radius_md,
|
||||
text_size: sizes.Size | str = sizes.text_md,
|
||||
font: fonts.Font | str | Iterable[fonts.Font | str] = (
|
||||
fonts.GoogleFont("Instrument Sans", weights=(400, 500, 600, 700)),
|
||||
"ui-sans-serif", "system-ui", "sans-serif",
|
||||
),
|
||||
font_mono: fonts.Font | str | Iterable[fonts.Font | str] = (
|
||||
fonts.GoogleFont("JetBrains Mono"),
|
||||
"ui-monospace", "Consolas", "monospace",
|
||||
),
|
||||
):
|
||||
super().__init__(
|
||||
primary_hue=primary_hue, secondary_hue=secondary_hue,
|
||||
neutral_hue=neutral_hue, spacing_size=spacing_size,
|
||||
radius_size=radius_size, text_size=text_size,
|
||||
font=font, font_mono=font_mono,
|
||||
)
|
||||
self.name = "my_theme"
|
||||
super().set(
|
||||
# Override variables here
|
||||
)
|
||||
```
|
||||
|
||||
**Always specify `weights=(...)` on `GoogleFont` if using anything outside the default `(400, 600)`.** Browsers will fake-bold missing weights, which looks awful.
|
||||
|
||||
## Building Blocks
|
||||
|
||||
**Colour palettes** — 22 named palettes, 11 shades each (`c50` lightest → `c950` darkest):
|
||||
`slate`, `gray`, `zinc`, `stone`, `neutral`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose`.
|
||||
|
||||
```python
|
||||
from gradio.themes.utils import colors
|
||||
colors.blue.c500 # "#3b82f6"
|
||||
f"{colors.violet.c800}60" # alpha hex (37.5% opacity)
|
||||
```
|
||||
|
||||
**Sizes** — 7 scales each (`xxs`–`xxl`):
|
||||
`radius_none`, `radius_sm`, `radius_md`, `radius_lg`, `radius_xxl`; `spacing_sm/md/lg`; `text_sm/md/lg`. Custom sizes via `Size(xxs=..., xs=..., ...)`.
|
||||
|
||||
**Fonts** — `GoogleFont(name, weights=(...))`, `LocalFont(name)`, plain strings for system fonts. Always include fallbacks in tuples.
|
||||
|
||||
## Variable Reference System
|
||||
|
||||
Use `*variable_name` to reference other theme variables. References resolve recursively at CSS generation time.
|
||||
|
||||
```python
|
||||
input_shadow="*shadow_drop"
|
||||
button_cancel_text_color="*button_secondary_text_color"
|
||||
```
|
||||
|
||||
**Dark mode references resolve automatically.** Do **not** append `_dark`:
|
||||
```python
|
||||
input_shadow_focus_dark="0 0 0 3px *primary_900" # CORRECT
|
||||
input_shadow_focus_dark="0 0 0 3px *primary_900_dark" # WRONG — error
|
||||
```
|
||||
|
||||
**Variables accept any CSS value** — colours, gradients, shadows, transforms, transitions, `none`, calc(), spacing tokens (`*spacing_md`).
|
||||
|
||||
**Full variable list** is in `gradio/themes/base.py` — search by name. Every variable accepts an optional `_dark` suffix.
|
||||
|
||||
### Non-obvious variables worth knowing
|
||||
|
||||
- `block_label_*` (media element titles like "Image", "Audio") vs `block_title_*` (form element titles like a Textbox label) — these are *different* and need to be styled together.
|
||||
- `body_background_fill` paints the actual page `<body>` (full viewport), not just the gradio container. To make the container itself transparent, set `background_fill_primary="transparent"`.
|
||||
- `button_transform_hover`/`_active` — for `translateY(-2px)` "lift" effects. Pair with `button_*_shadow_hover` for proper depth.
|
||||
- `button_{size}_*` (large/small) controls padding/radius/text size per size; `button_{variant}_*` (primary/secondary/cancel) controls colours/shadows per variant.
|
||||
- `checkbox_label_*` is the surrounding pill button, separate from `checkbox_*` (the box itself).
|
||||
- `stat_background_fill` accepts gradients — useful for confidence bars.
|
||||
|
||||
## Custom CSS
|
||||
|
||||
Themes can bundle arbitrary CSS via `self.custom_css` (set in `__init__`). It's injected alongside the variables and ships with the theme when published to the Hub.
|
||||
|
||||
```python
|
||||
class MyTheme(Base):
|
||||
def __init__(self, ...):
|
||||
super().__init__(...)
|
||||
self.name = "my_theme"
|
||||
self.custom_css = """
|
||||
...
|
||||
"""
|
||||
super().set(...)
|
||||
```
|
||||
|
||||
**Use `custom_css` for things variables can't express:** `backdrop-filter`, tiling background images, custom slider thumbs, pseudo-element decorations, targeting specific Gradio DOM (`.label-wrap`, `button.secondary`, `.reset-button`, `input[type="range"]`).
|
||||
|
||||
### Critical gotcha: Shadow DOM scope
|
||||
|
||||
Theme CSS is injected inside the `<gradio-app>` shadow DOM. **Selectors targeting `html` or `body` will not work** — those elements live in the light DOM (the actual page document).
|
||||
|
||||
To paint the page background, use the `body_background_fill` variable (Gradio applies it to the real `<body>` from `+layout.svelte`). Do *not* try to style `body` from `custom_css`.
|
||||
|
||||
```python
|
||||
# CORRECT — paints actual <body>, full viewport
|
||||
body_background_fill="linear-gradient(...)"
|
||||
|
||||
# WRONG — selector doesn't resolve, gradient never paints
|
||||
self.custom_css = "body { background: linear-gradient(...) }"
|
||||
```
|
||||
|
||||
### Custom slider thumbs
|
||||
|
||||
Must include both webkit and moz prefixes; need `!important` to beat Gradio's defaults:
|
||||
```css
|
||||
input[type="range"]::-webkit-slider-thumb,
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
appearance: none !important;
|
||||
width: 30px !important;
|
||||
height: 30px !important;
|
||||
background: url("data:image/png;base64,...") no-repeat center / contain !important;
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
```
|
||||
|
||||
### Gradio DOM selectors
|
||||
|
||||
Generic class names (no Svelte hashes on these): `.gradio-container`, `.block`, `.panel`, `.form`, `.wrap`, `.label-wrap`, `button.primary`, `button.secondary`, `.reset-button`, `input[type="range"]`. Dark mode: `.dark .xxx`. Inspect the live DOM to find anything else — class names with hashes can change between versions.
|
||||
|
||||
## Building from a Reference Image
|
||||
|
||||
When matching a screenshot:
|
||||
|
||||
1. **Extract**: background (solid/gradient/texture, exact colours), card style (border, radius, shadow), text weight/colour, accent hue, font feel, distinctive elements.
|
||||
2. **Map**: bg → `body_background_fill`; cards → `block_*`; buttons → `button_*` + `custom_css` for complex gradients/glows; accent → custom `Color()` if no palette matches.
|
||||
3. **Build order**: background → blocks → buttons → inputs/labels → details (slider thumbs, focus rings).
|
||||
4. **Pitfalls**: large border-radius + Gradio's `overflow: hidden` clips content (cap at ~20px); complex multi-stop button gradients need `custom_css` with `!important`; backdrop-filter doesn't work in Firefox by default.
|
||||
|
||||
## Pre-Shipping Checklist
|
||||
|
||||
1. `self.name` set in `__init__`
|
||||
2. **Text contrast audit** (do this first):
|
||||
- Body text on body/block backgrounds
|
||||
- Label text on coloured label fills (contrast against the *fill*, not the page)
|
||||
- Button text on button fills (all 3 variants: primary, secondary, cancel)
|
||||
- Placeholder text — visible but distinct from entered text (≥ #999 on white)
|
||||
- Selected checkbox/radio text on selected fill
|
||||
- Error text on error background
|
||||
- Link text on body background
|
||||
3. Light mode: body, blocks, inputs, buttons, labels, checkboxes, tables
|
||||
4. Dark mode: same elements, *independently designed* (not auto-inverted)
|
||||
5. Focus, hover, active, selected states all 3 button variants
|
||||
6. **Aesthetic Quality pass**: AI slop test, no palette/typography traps, hierarchy works at squint distance
|
||||
7. Font weights all explicitly loaded via `weights=(...)`
|
||||
8. Test with `gr.themes.builder()` for interactive preview
|
||||
|
||||
## Publishing
|
||||
|
||||
```python
|
||||
theme.push_to_hub(
|
||||
repo_name="my-theme",
|
||||
org_name="my-org",
|
||||
version="0.0.1",
|
||||
description="A bold theme for data dashboards.",
|
||||
)
|
||||
|
||||
# Load
|
||||
theme = gr.themes.Theme.from_hub("my-org/my-theme@1.2.0")
|
||||
```
|
||||
|
||||
`custom_css` is bundled automatically. Save/load locally via `theme.dump("my_theme.json")` / `Theme.load(...)`.
|
||||
|
||||
## Registering a Built-in Theme
|
||||
|
||||
1. Create `gradio/themes/my_theme.py`
|
||||
2. Add `from gradio.themes.my_theme import MyTheme` to `gradio/themes/__init__.py` and add `"MyTheme"` to `__all__`
|
||||
3. Set `self.name = "my_theme"` in `__init__`
|
||||
|
||||
## Reference: Exemplar Theme Files
|
||||
|
||||
Read these directly for concrete patterns. Do not re-implement what exists:
|
||||
|
||||
| Theme | Style | Techniques to study |
|
||||
|-------|-------|---------------------|
|
||||
| `gradio/themes/soft.py` | Minimal, soft | Shadow-based depth, no block borders, rounded labels |
|
||||
| `gradio/themes/cyberpunk.py` | Bold, neon | Custom hex dark backgrounds, neon glow shadows, alpha colours |
|
||||
| `gradio/themes/neon.py` | Playful, raised | Bottom-edge shadows, transform hover/active, pill shapes |
|
||||
| `gradio/themes/ember.py` | Warm, polished | Comprehensive coverage, focus ring shadows |
|
||||
| `gradio/themes/ocean.py` | Gradient, fluid | CSS gradients in buttons + checkbox labels, scale transforms |
|
||||
| `gradio/themes/glass.py` | Editorial, subtle | Gradient fills on inputs/buttons, system fonts |
|
||||
| `gradio/themes/monochrome.py` | Sharp, no colour | All neutral hues, serif font, sharp radius, thick borders |
|
||||
| `gradio/themes/default.py` | Balanced, standard | Orange+blue dual hue, stat gradients, error colours |
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
name: gradio
|
||||
description: Build Gradio web UIs and demos in Python. Use when creating, modifying, debugging, or answering questions about Gradio and its capabilties, components, event listeners, or layouts.
|
||||
---
|
||||
|
||||
# Gradio
|
||||
|
||||
Gradio is a Python library for building interactive web UIs and ML demos. This skill covers the core API, patterns, and examples.
|
||||
|
||||
## References
|
||||
- `references/examples.md` - Illustrative examples showcasing the core Gradio API.
|
||||
- `references/api-signatures.md` - API signatures of commonly used components.
|
||||
- `references/event-listeners.md` - API signatures of events supported by each component.
|
||||
|
||||
## Guides
|
||||
|
||||
Detailed guides on specific topics (read these when relevant):
|
||||
|
||||
- [Quickstart](https://www.gradio.app/guides/quickstart)
|
||||
- [The Interface Class](https://www.gradio.app/guides/the-interface-class)
|
||||
- [Blocks and Event Listeners](https://www.gradio.app/guides/blocks-and-event-listeners)
|
||||
- [Controlling Layout](https://www.gradio.app/guides/controlling-layout)
|
||||
- [More Blocks Features](https://www.gradio.app/guides/more-blocks-features)
|
||||
- [Custom CSS and JS](https://www.gradio.app/guides/custom-CSS-and-JS)
|
||||
- [Streaming Outputs](https://www.gradio.app/guides/streaming-outputs)
|
||||
- [Streaming Inputs](https://www.gradio.app/guides/streaming-inputs)
|
||||
- [Sharing Your App](https://www.gradio.app/guides/sharing-your-app)
|
||||
- [Custom HTML Components](https://www.gradio.app/guides/custom-HTML-components)
|
||||
- [Getting Started with the Python Client](https://www.gradio.app/guides/getting-started-with-the-python-client)
|
||||
- [Getting Started with the JS Client](https://www.gradio.app/guides/getting-started-with-the-js-client)
|
||||
|
||||
## Core Patterns
|
||||
|
||||
**Interface** (high-level): wraps a function with input/output components.
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
|
||||
def greet(name):
|
||||
return f"Hello {name}!"
|
||||
|
||||
gr.Interface(fn=greet, inputs="text", outputs="text").launch()
|
||||
```
|
||||
|
||||
**Blocks** (low-level): flexible layout with explicit event wiring.
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
name = gr.Textbox(label="Name")
|
||||
output = gr.Textbox(label="Greeting")
|
||||
btn = gr.Button("Greet")
|
||||
btn.click(fn=lambda n: f"Hello {n}!", inputs=name, outputs=output)
|
||||
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
**ChatInterface**: high-level wrapper for chatbot UIs.
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
|
||||
def respond(message, history):
|
||||
return f"You said: {message}"
|
||||
|
||||
gr.ChatInterface(fn=respond).launch()
|
||||
```
|
||||
|
||||
## Custom HTML Components
|
||||
|
||||
If a task requires significant customization of an existing component or a component that doesn't exist in Gradio, you can create one with `gr.HTML`. It supports `html_template` (with `${}` JS expressions and `{{}}` Handlebars syntax), `css_template` for scoped styles, and `js_on_load` for interactivity — where `props.value` updates the component value and `trigger('event_name')` fires Gradio events. For reuse, subclass `gr.HTML` and define `api_info()` for API/MCP support.
|
||||
|
||||
See the [full guide](https://www.gradio.app/guides/custom-HTML-components) as well as example in `references/examples.md`
|
||||
|
||||
## Server Mode
|
||||
|
||||
Use `gr.Server` instead of gr.Blocks when the users requests any of the following:
|
||||
- Completely custom UI (your own HTML, React, Svelte, etc.) powered by Gradio's backend.
|
||||
- Full control of FastAPI server (custom GET/POST routes, middleware, dependency injection) alongside Gradio API endpoints
|
||||
|
||||
See the [full guide](https://www.gradio.app/guides/server-mode) and example in `references/examples.md`.
|
||||
|
||||
If the user's use case can be handled by Gradio's built-in components or customizable HTML components, prefer not to use `gr.Server`.
|
||||
@@ -0,0 +1,115 @@
|
||||
# Component API Signatures
|
||||
|
||||
Quick reference for common Gradio component constructors.
|
||||
|
||||
## `Textbox`
|
||||
|
||||
```python
|
||||
Textbox(value: str | I18nData | Callable | None = None, type: Literal['text', 'password', 'email'] = "text", lines: int = 1, max_lines: int | None = None, placeholder: str | I18nData | None = None, label: str | I18nData | None = None, info: str | I18nData | None = None, every: Timer | float | None = None, inputs: Component | Sequence[Component] | set[Component] | None = None, show_label: bool | None = None, container: bool = True, scale: int | None = None, min_width: int = 160, interactive: bool | None = None, visible: bool | Literal['hidden'] = True, elem_id: str | None = None, autofocus: bool = False, autoscroll: bool = True, elem_classes: list[str] | str | None = None, render: bool = True, key: int | str | tuple[int | str, ...] | None = None, preserved_by_key: list[str] | str | None = "value", text_align: Literal['left', 'right'] | None = None, rtl: bool = False, buttons: list[Literal['copy'] | Button] | None = None, max_length: int | None = None, submit_btn: str | bool | None = False, stop_btn: str | bool | None = False, html_attributes: InputHTMLAttributes | None = None)
|
||||
```
|
||||
|
||||
Creates a textarea for user to enter string input or display string output.
|
||||
|
||||
## `Number`
|
||||
|
||||
```python
|
||||
Number(value: float | Callable | None = None, label: str | I18nData | None = None, placeholder: str | I18nData | None = None, info: str | I18nData | None = None, every: Timer | float | None = None, inputs: Component | Sequence[Component] | set[Component] | None = None, show_label: bool | None = None, container: bool = True, scale: int | None = None, min_width: int = 160, interactive: bool | None = None, visible: bool | Literal['hidden'] = True, elem_id: str | None = None, elem_classes: list[str] | str | None = None, render: bool = True, key: int | str | tuple[int | str, ...] | None = None, preserved_by_key: list[str] | str | None = "value", buttons: list[Button] | None = None, precision: int | None = None, minimum: float | None = None, maximum: float | None = None, step: float = 1)
|
||||
```
|
||||
|
||||
Creates a numeric field for user to enter numbers as input or display numeric output.
|
||||
|
||||
## `Slider`
|
||||
|
||||
```python
|
||||
Slider(minimum: float = 0, maximum: float = 100, value: float | Callable | None = None, step: float | None = None, precision: int | None = None, label: str | I18nData | None = None, info: str | I18nData | None = None, every: Timer | float | None = None, inputs: Component | Sequence[Component] | set[Component] | None = None, show_label: bool | None = None, container: bool = True, scale: int | None = None, min_width: int = 160, interactive: bool | None = None, visible: bool | Literal['hidden'] = True, elem_id: str | None = None, elem_classes: list[str] | str | None = None, render: bool = True, key: int | str | tuple[int | str, ...] | None = None, preserved_by_key: list[str] | str | None = "value", randomize: bool = False, buttons: list[Literal['reset']] | None = None)
|
||||
```
|
||||
|
||||
Creates a slider that ranges from {minimum} to {maximum} with a step size of {step}.
|
||||
|
||||
## `Checkbox`
|
||||
|
||||
```python
|
||||
Checkbox(value: bool | Callable = False, label: str | I18nData | None = None, info: str | I18nData | None = None, every: Timer | float | None = None, inputs: Component | Sequence[Component] | set[Component] | None = None, show_label: bool | None = None, container: bool = True, scale: int | None = None, min_width: int = 160, interactive: bool | None = None, visible: bool | Literal['hidden'] = True, elem_id: str | None = None, elem_classes: list[str] | str | None = None, render: bool = True, key: int | str | tuple[int | str, ...] | None = None, preserved_by_key: list[str] | str | None = "value", buttons: list[Button] | None = None)
|
||||
```
|
||||
|
||||
Creates a checkbox that can be set to `True` or `False`. Can be used as an input to pass a boolean value to a function or as an output to display a boolean value.
|
||||
|
||||
## `Dropdown`
|
||||
|
||||
```python
|
||||
Dropdown(choices: Sequence[str | int | float | tuple[str | I18nData, str | int | float]] | None = None, value: str | int | float | Sequence[str | int | float] | Callable | DefaultValue | None = DefaultValue(), type: Literal['value', 'index'] = "value", multiselect: bool | None = None, allow_custom_value: bool = False, max_choices: int | None = None, filterable: bool = True, label: str | I18nData | None = None, info: str | I18nData | None = None, every: Timer | float | None = None, inputs: Component | Sequence[Component] | set[Component] | None = None, show_label: bool | None = None, container: bool = True, scale: int | None = None, min_width: int = 160, interactive: bool | None = None, visible: bool | Literal['hidden'] = True, elem_id: str | None = None, elem_classes: list[str] | str | None = None, render: bool = True, key: int | str | tuple[int | str, ...] | None = None, preserved_by_key: list[str] | str | None = "value", buttons: list[Button] | None = None)
|
||||
```
|
||||
|
||||
Creates a dropdown of choices from which a single entry or multiple entries can be selected (as an input component) or displayed (as an output component).
|
||||
|
||||
## `Radio`
|
||||
|
||||
```python
|
||||
Radio(choices: Sequence[str | int | float | tuple[str | I18nData, str | int | float]] | None = None, value: str | int | float | Callable | None = None, type: Literal['value', 'index'] = "value", label: str | I18nData | None = None, info: str | I18nData | None = None, every: Timer | float | None = None, inputs: Component | Sequence[Component] | set[Component] | None = None, show_label: bool | None = None, container: bool = True, scale: int | None = None, min_width: int = 160, interactive: bool | None = None, visible: bool | Literal['hidden'] = True, elem_id: str | None = None, elem_classes: list[str] | str | None = None, render: bool = True, key: int | str | tuple[int | str, ...] | None = None, preserved_by_key: list[str] | str | None = "value", rtl: bool = False, buttons: list[Button] | None = None)
|
||||
```
|
||||
|
||||
Creates a set of (string or numeric type) radio buttons of which only one can be selected.
|
||||
|
||||
## `Image`
|
||||
|
||||
```python
|
||||
Image(value: str | PIL.Image.Image | np.ndarray | Callable | None = None, format: str = "webp", height: int | str | None = None, width: int | str | None = None, image_mode: Literal['1', 'L', 'P', 'RGB', 'RGBA', 'CMYK', 'YCbCr', 'LAB', 'HSV', 'I', 'F'] | None = "RGB", sources: list[Literal['upload', 'webcam', 'clipboard']] | Literal['upload', 'webcam', 'clipboard'] | None = None, type: Literal['numpy', 'pil', 'filepath'] = "numpy", label: str | I18nData | None = None, every: Timer | float | None = None, inputs: Component | Sequence[Component] | set[Component] | None = None, show_label: bool | None = None, buttons: list[Literal['download', 'share', 'fullscreen'] | Button] | None = None, container: bool = True, scale: int | None = None, min_width: int = 160, interactive: bool | None = None, visible: bool | Literal['hidden'] = True, streaming: bool = False, elem_id: str | None = None, elem_classes: list[str] | str | None = None, render: bool = True, key: int | str | tuple[int | str, ...] | None = None, preserved_by_key: list[str] | str | None = "value", webcam_options: WebcamOptions | None = None, placeholder: str | None = None, watermark: WatermarkOptions | None = None)
|
||||
```
|
||||
|
||||
Creates an image component that can be used to upload images (as an input) or display images (as an output).
|
||||
|
||||
## `Audio`
|
||||
|
||||
```python
|
||||
Audio(value: str | Path | tuple[int, np.ndarray] | Callable | None = None, sources: list[Literal['upload', 'microphone']] | Literal['upload', 'microphone'] | None = None, type: Literal['numpy', 'filepath'] = "numpy", label: str | I18nData | None = None, every: Timer | float | None = None, inputs: Component | Sequence[Component] | set[Component] | None = None, show_label: bool | None = None, container: bool = True, scale: int | None = None, min_width: int = 160, interactive: bool | None = None, visible: bool | Literal['hidden'] = True, streaming: bool = False, elem_id: str | None = None, elem_classes: list[str] | str | None = None, render: bool = True, key: int | str | tuple[int | str, ...] | None = None, preserved_by_key: list[str] | str | None = "value", format: Literal['wav', 'mp3'] | None = None, autoplay: bool = False, editable: bool = True, buttons: list[Literal['download', 'share'] | Button] | None = None, waveform_options: WaveformOptions | dict | None = None, loop: bool = False, recording: bool = False, subtitles: str | Path | list[dict[str, Any]] | None = None, playback_position: float = 0)
|
||||
```
|
||||
|
||||
Creates an audio component that can be used to upload/record audio (as an input) or display audio (as an output).
|
||||
|
||||
## `Video`
|
||||
|
||||
```python
|
||||
Video(value: str | Path | Callable | None = None, format: str | None = None, sources: list[Literal['upload', 'webcam']] | Literal['upload', 'webcam'] | None = None, height: int | str | None = None, width: int | str | None = None, label: str | I18nData | None = None, every: Timer | float | None = None, inputs: Component | Sequence[Component] | set[Component] | None = None, show_label: bool | None = None, container: bool = True, scale: int | None = None, min_width: int = 160, interactive: bool | None = None, visible: bool | Literal['hidden'] = True, elem_id: str | None = None, elem_classes: list[str] | str | None = None, render: bool = True, key: int | str | tuple[int | str, ...] | None = None, preserved_by_key: list[str] | str | None = "value", webcam_options: WebcamOptions | None = None, include_audio: bool | None = None, autoplay: bool = False, buttons: list[Literal['download', 'share'] | Button] | None = None, loop: bool = False, streaming: bool = False, watermark: WatermarkOptions | None = None, subtitles: str | Path | list[dict[str, Any]] | None = None, playback_position: float = 0)
|
||||
```
|
||||
|
||||
Creates a video component that can be used to upload/record videos (as an input) or display videos (as an output). For the video to be playable in the browser it must have a compatible container and codec combination. Allowed combinations are .mp4 with h264 codec, .ogg with theora codec, and .webm with vp9 codec. If the component detects that the output video would not be playable in the browser it will attempt to convert it to a playable mp4 video. If the conversion fails, the original video is returned.
|
||||
|
||||
## `File`
|
||||
|
||||
```python
|
||||
File(value: str | list[str] | Callable | None = None, file_count: Literal['single', 'multiple', 'directory'] = "single", file_types: list[str] | None = None, type: Literal['filepath', 'binary'] = "filepath", label: str | I18nData | None = None, every: Timer | float | None = None, inputs: Component | Sequence[Component] | set[Component] | None = None, show_label: bool | None = None, container: bool = True, scale: int | None = None, min_width: int = 160, height: int | str | float | None = None, interactive: bool | None = None, visible: bool | Literal['hidden'] = True, elem_id: str | None = None, elem_classes: list[str] | str | None = None, render: bool = True, key: int | str | tuple[int | str, ...] | None = None, preserved_by_key: list[str] | str | None = "value", allow_reordering: bool = False, buttons: list[Button] | None = None)
|
||||
```
|
||||
|
||||
Creates a file component that allows uploading one or more generic files (when used as an input) or displaying generic files or URLs for download (as output). Demo: zip_files, zip_to_json
|
||||
|
||||
## `Chatbot`
|
||||
|
||||
```python
|
||||
Chatbot(value: list[MessageDict | Message] | Callable | None = None, label: str | I18nData | None = None, every: Timer | float | None = None, inputs: Component | Sequence[Component] | set[Component] | None = None, show_label: bool | None = None, container: bool = True, scale: int | None = None, min_width: int = 160, visible: bool | Literal['hidden'] = True, elem_id: str | None = None, elem_classes: list[str] | str | None = None, autoscroll: bool = True, render: bool = True, key: int | str | tuple[int | str, ...] | None = None, preserved_by_key: list[str] | str | None = "value", height: int | str | None = 400, resizable: bool = False, max_height: int | str | None = None, min_height: int | str | None = None, editable: Literal['user', 'all'] | None = None, latex_delimiters: list[dict[str, str | bool]] | None = None, rtl: bool = False, buttons: list[Literal['share', 'copy', 'copy_all'] | Button] | None = None, watermark: str | None = None, avatar_images: tuple[str | Path | None, str | Path | None] | None = None, sanitize_html: bool = True, render_markdown: bool = True, feedback_options: list[str] | tuple[str, ...] | None = ('Like', 'Dislike'), feedback_value: Sequence[str | None] | None = None, line_breaks: bool = True, layout: Literal['panel', 'bubble'] | None = None, placeholder: str | None = None, examples: list[ExampleMessage] | None = None, allow_file_downloads: bool = True, group_consecutive_messages: bool = True, allow_tags: list[str] | bool = True, reasoning_tags: list[tuple[str, str]] | None = None, like_user_message: bool = False)
|
||||
```
|
||||
|
||||
Creates a chatbot that displays user-submitted messages and responses. Supports a subset of Markdown including bold, italics, code, tables. Also supports audio/video/image files, which are displayed in the Chatbot, and other kinds of files which are displayed as links. This component is usually used as an output component.
|
||||
|
||||
## `Button`
|
||||
|
||||
```python
|
||||
Button(value: str | I18nData | Callable = "Run", every: Timer | float | None = None, inputs: Component | Sequence[Component] | set[Component] | None = None, variant: Literal['primary', 'secondary', 'stop', 'huggingface'] = "secondary", size: Literal['sm', 'md', 'lg'] = "lg", icon: str | Path | None = None, link: str | None = None, link_target: Literal['_self', '_blank', '_parent', '_top'] = "_self", visible: bool | Literal['hidden'] = True, interactive: bool = True, elem_id: str | None = None, elem_classes: list[str] | str | None = None, render: bool = True, key: int | str | tuple[int | str, ...] | None = None, preserved_by_key: list[str] | str | None = "value", scale: int | None = None, min_width: int | None = None)
|
||||
```
|
||||
|
||||
Creates a button that can be assigned arbitrary .click() events. The value (label) of the button can be used as an input to the function (rarely used) or set via the output of a function.
|
||||
|
||||
## `Markdown`
|
||||
|
||||
```python
|
||||
Markdown(value: str | I18nData | Callable | None = None, label: str | I18nData | None = None, every: Timer | float | None = None, inputs: Component | Sequence[Component] | set[Component] | None = None, show_label: bool | None = None, scale: int | None = None, min_width: int | None = None, rtl: bool = False, latex_delimiters: list[dict[str, str | bool]] | None = None, visible: bool | Literal['hidden'] = True, elem_id: str | None = None, elem_classes: list[str] | str | None = None, render: bool = True, key: int | str | tuple[int | str, ...] | None = None, preserved_by_key: list[str] | str | None = "value", sanitize_html: bool = True, line_breaks: bool = False, header_links: bool = False, height: int | str | None = None, max_height: int | str | None = None, min_height: int | str | None = None, buttons: list[Literal['copy']] | None = None, container: bool = False, padding: bool = False)
|
||||
```
|
||||
|
||||
Used to render arbitrary Markdown output. Can also render latex enclosed by dollar signs as well as code blocks with syntax highlighting. Supported languages are bash, c, cpp, go, java, javascript, json, php, python, rust, sql, and yaml. As this component does not accept user input, it is rarely used as an input component.
|
||||
|
||||
## `HTML`
|
||||
|
||||
```python
|
||||
HTML(value: Any | Callable | None = None, label: str | I18nData | None = None, html_template: str = "${value}", css_template: str = "", js_on_load: str | None = "element.addEventListener('click', function() { trigger('click') });", apply_default_css: bool = True, every: Timer | float | None = None, inputs: Component | Sequence[Component] | set[Component] | None = None, show_label: bool = False, scale: int | None = None, min_width: int | None = None, visible: bool | Literal['hidden'] = True, elem_id: str | None = None, elem_classes: list[str] | str | None = None, render: bool = True, key: int | str | tuple[int | str, ...] | None = None, preserved_by_key: list[str] | str | None = "value", min_height: int | None = None, max_height: int | None = None, container: bool = False, padding: bool = False, autoscroll: bool = False, buttons: list[Button] | None = None, head: str | None = None, server_functions: list[Callable] | None = None, props: Any)
|
||||
```
|
||||
|
||||
Creates a component with arbitrary HTML. Can include CSS and JavaScript to create highly customized and interactive components.
|
||||
@@ -0,0 +1,132 @@
|
||||
# Event Listeners
|
||||
|
||||
Events supported by each component.
|
||||
|
||||
|
||||
## Event Listener Signature
|
||||
|
||||
```python
|
||||
component.event_name(
|
||||
fn: Callable | None | Literal["decorator"] = "decorator",
|
||||
inputs: Component | Sequence[Component] | set[Component] | None = None,
|
||||
outputs: Component | Sequence[Component] | set[Component] | None = None,
|
||||
api_name: str | None = None,
|
||||
api_description: str | None | Literal[False] = None,
|
||||
scroll_to_output: bool = False,
|
||||
show_progress: Literal["full", "minimal", "hidden"] = "full",
|
||||
show_progress_on: Component | Sequence[Component] | None = None,
|
||||
queue: bool = True,
|
||||
batch: bool = False,
|
||||
max_batch_size: int = 4,
|
||||
preprocess: bool = True,
|
||||
postprocess: bool = True,
|
||||
cancels: dict[str, Any] | list[dict[str, Any]] | None = None,
|
||||
trigger_mode: Literal["once", "multiple", "always_last"] | None = None,
|
||||
js: str | Literal[True] | None = None,
|
||||
concurrency_limit: int | None | Literal["default"] = "default",
|
||||
concurrency_id: str | None = None,
|
||||
api_visibility: Literal["public", "private", "undocumented"] = "public",
|
||||
time_limit: int | None = None,
|
||||
stream_every: float = 0.5,
|
||||
key: int | str | tuple[int | str, ...] | None = None,
|
||||
validator: Callable | None = None,
|
||||
) -> Dependency
|
||||
```
|
||||
|
||||
## Supported Events by Component
|
||||
|
||||
- **AnnotatedImage**: change, select
|
||||
|
||||
- **Audio**: stream, change, clear, play, pause, stop, pause, start_recording, pause_recording, stop_recording, upload, input
|
||||
|
||||
- **BarPlot**: change, select, double_click
|
||||
|
||||
- **BrowserState**: change
|
||||
|
||||
- **Button**: change, click
|
||||
|
||||
- **Chatbot**: change, select, like, retry, undo, example_select, option_select, clear, copy, edit
|
||||
|
||||
- **Checkbox**: change, input, select
|
||||
|
||||
- **CheckboxGroup**: change, input, select
|
||||
|
||||
- **ClearButton**: change, click
|
||||
|
||||
- **Code**: change, input, focus, blur
|
||||
|
||||
- **ColorPicker**: change, input, release, submit, focus, blur
|
||||
|
||||
- **Dataframe**: change, input, select, edit
|
||||
|
||||
- **Dataset**: change, click, select
|
||||
|
||||
- **DateTime**: change, submit
|
||||
|
||||
- **DeepLinkButton**: change, click
|
||||
|
||||
- **Dialogue**: change, input, submit
|
||||
|
||||
- **DownloadButton**: change, click
|
||||
|
||||
- **Dropdown**: change, input, select, focus, blur, key_up
|
||||
|
||||
- **DuplicateButton**: change, click
|
||||
|
||||
- **File**: change, select, clear, upload, delete, download
|
||||
|
||||
- **FileExplorer**: change, input, select
|
||||
|
||||
- **Gallery**: select, upload, change, delete, preview_close, preview_open
|
||||
|
||||
- **HTML**: change, input, click, double_click, submit, stop, edit, clear, play, pause, end, start_recording, pause_recording, stop_recording, focus, blur, upload, release, select, stream, like, example_select, option_select, load, key_up, apply, delete, tick, undo, retry, expand, collapse, download, copy
|
||||
|
||||
- **HighlightedText**: change, select
|
||||
|
||||
- **Image**: clear, change, stream, select, upload, input
|
||||
|
||||
- **ImageEditor**: clear, change, input, select, upload, apply
|
||||
|
||||
- **ImageSlider**: clear, change, stream, select, upload, input
|
||||
|
||||
- **JSON**: change
|
||||
|
||||
- **Label**: change, select
|
||||
|
||||
- **LinePlot**: change, select, double_click
|
||||
|
||||
- **LoginButton**: change, click
|
||||
|
||||
- **Markdown**: change, copy
|
||||
|
||||
- **Model3D**: change, upload, edit, clear
|
||||
|
||||
- **MultimodalTextbox**: change, input, select, submit, focus, blur, stop
|
||||
|
||||
- **Navbar**: change
|
||||
|
||||
- **Number**: change, input, submit, focus, blur
|
||||
|
||||
- **ParamViewer**: change, upload
|
||||
|
||||
- **Plot**: change
|
||||
|
||||
- **Radio**: select, change, input
|
||||
|
||||
- **ScatterPlot**: change, select, double_click
|
||||
|
||||
- **SimpleImage**: clear, change, upload
|
||||
|
||||
- **Slider**: change, input, release
|
||||
|
||||
- **State**: change
|
||||
|
||||
- **Textbox**: change, input, select, submit, focus, blur, stop, copy
|
||||
|
||||
- **Timer**: change, tick
|
||||
|
||||
- **UploadButton**: change, click, upload
|
||||
|
||||
- **Video**: change, clear, start_recording, stop_recording, stop, play, pause, end, upload, input
|
||||
|
||||
- **WorkflowCanvas**: change
|
||||
@@ -0,0 +1,613 @@
|
||||
# Gradio End-to-End Examples
|
||||
|
||||
Complete working Gradio apps for reference.
|
||||
|
||||
## Blocks Essay Simple
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
|
||||
def change_textbox(choice):
|
||||
if choice == "short":
|
||||
return gr.Textbox(lines=2, visible=True)
|
||||
elif choice == "long":
|
||||
return gr.Textbox(lines=8, visible=True, value="Lorem ipsum dolor sit amet")
|
||||
else:
|
||||
return gr.Textbox(visible=False)
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
radio = gr.Radio(
|
||||
["short", "long", "none"], label="What kind of essay would you like to write?"
|
||||
)
|
||||
text = gr.Textbox(lines=2, interactive=True, buttons=["copy"])
|
||||
radio.change(fn=change_textbox, inputs=radio, outputs=text)
|
||||
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
## Blocks Flipper
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import gradio as gr
|
||||
|
||||
def flip_text(x):
|
||||
return x[::-1]
|
||||
|
||||
def flip_image(x):
|
||||
return np.fliplr(x)
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
gr.Markdown("Flip text or image files using this demo.")
|
||||
with gr.Tab("Flip Text"):
|
||||
text_input = gr.Textbox()
|
||||
text_output = gr.Textbox()
|
||||
text_button = gr.Button("Flip")
|
||||
with gr.Tab("Flip Image"):
|
||||
with gr.Row():
|
||||
image_input = gr.Image()
|
||||
image_output = gr.Image()
|
||||
image_button = gr.Button("Flip")
|
||||
|
||||
with gr.Accordion("Open for More!", open=False):
|
||||
gr.Markdown("Look at me...")
|
||||
temp_slider = gr.Slider(
|
||||
0, 1,
|
||||
value=0.1,
|
||||
step=0.1,
|
||||
interactive=True,
|
||||
label="Slide me",
|
||||
)
|
||||
|
||||
text_button.click(flip_text, inputs=text_input, outputs=text_output)
|
||||
image_button.click(flip_image, inputs=image_input, outputs=image_output)
|
||||
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
## Blocks Form
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
name_box = gr.Textbox(label="Name")
|
||||
age_box = gr.Number(label="Age", minimum=0, maximum=100)
|
||||
symptoms_box = gr.CheckboxGroup(["Cough", "Fever", "Runny Nose"])
|
||||
submit_btn = gr.Button("Submit")
|
||||
|
||||
with gr.Column(visible=False) as output_col:
|
||||
diagnosis_box = gr.Textbox(label="Diagnosis")
|
||||
patient_summary_box = gr.Textbox(label="Patient Summary")
|
||||
|
||||
def submit(name, age, symptoms):
|
||||
return {
|
||||
submit_btn: gr.Button(visible=False),
|
||||
output_col: gr.Column(visible=True),
|
||||
diagnosis_box: "covid" if "Cough" in symptoms else "flu",
|
||||
patient_summary_box: f"{name}, {age} y/o",
|
||||
}
|
||||
|
||||
submit_btn.click(
|
||||
submit,
|
||||
[name_box, age_box, symptoms_box],
|
||||
[submit_btn, diagnosis_box, patient_summary_box, output_col],
|
||||
)
|
||||
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
## Blocks Hello
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
|
||||
def welcome(name):
|
||||
return f"Welcome to Gradio, {name}!"
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
gr.Markdown(
|
||||
"""
|
||||
# Hello World!
|
||||
Start typing below to see the output.
|
||||
""")
|
||||
inp = gr.Textbox(placeholder="What is your name?")
|
||||
out = gr.Textbox()
|
||||
inp.change(welcome, inp, out)
|
||||
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
## Blocks Layout
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
|
||||
demo = gr.Blocks()
|
||||
|
||||
with demo:
|
||||
with gr.Row():
|
||||
gr.Image(interactive=True, scale=2)
|
||||
gr.Image()
|
||||
with gr.Row():
|
||||
gr.Textbox(label="Text")
|
||||
gr.Number(label="Count", scale=2)
|
||||
gr.Radio(choices=["One", "Two"])
|
||||
with gr.Row():
|
||||
gr.Button("500", scale=0, min_width=500)
|
||||
gr.Button("A", scale=0)
|
||||
gr.Button("grow")
|
||||
with gr.Row():
|
||||
gr.Textbox()
|
||||
gr.Textbox()
|
||||
gr.Button()
|
||||
with gr.Row():
|
||||
with gr.Row():
|
||||
with gr.Column():
|
||||
gr.Textbox(label="Text")
|
||||
gr.Number(label="Count")
|
||||
gr.Radio(choices=["One", "Two"])
|
||||
gr.Image()
|
||||
with gr.Column():
|
||||
gr.Image(interactive=True)
|
||||
gr.Image()
|
||||
gr.Image()
|
||||
gr.Textbox(label="Text")
|
||||
gr.Number(label="Count")
|
||||
gr.Radio(choices=["One", "Two"])
|
||||
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
## Calculator
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
|
||||
def calculator(num1, operation, num2):
|
||||
if operation == "add":
|
||||
return num1 + num2
|
||||
elif operation == "subtract":
|
||||
return num1 - num2
|
||||
elif operation == "multiply":
|
||||
return num1 * num2
|
||||
elif operation == "divide":
|
||||
if num2 == 0:
|
||||
raise gr.Error("Cannot divide by zero!")
|
||||
return num1 / num2
|
||||
|
||||
demo = gr.Interface(
|
||||
calculator,
|
||||
[
|
||||
"number",
|
||||
gr.Radio(["add", "subtract", "multiply", "divide"]),
|
||||
"number"
|
||||
],
|
||||
"number",
|
||||
examples=[
|
||||
[45, "add", 3],
|
||||
[3.14, "divide", 2],
|
||||
[144, "multiply", 2.5],
|
||||
[0, "subtract", 1.2],
|
||||
],
|
||||
title="Toy Calculator",
|
||||
description="Here's a sample toy calculator.",
|
||||
api_name="predict"
|
||||
)
|
||||
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
## Chatbot Simple
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
import random
|
||||
import time
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
chatbot = gr.Chatbot()
|
||||
msg = gr.Textbox()
|
||||
clear = gr.ClearButton([msg, chatbot])
|
||||
|
||||
def respond(message, chat_history):
|
||||
bot_message = random.choice(["How are you?", "Today is a great day", "I'm very hungry"])
|
||||
chat_history.append({"role": "user", "content": message})
|
||||
chat_history.append({"role": "assistant", "content": bot_message})
|
||||
time.sleep(2)
|
||||
return "", chat_history
|
||||
|
||||
msg.submit(respond, [msg, chatbot], [msg, chatbot])
|
||||
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
## Chatbot Streaming
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
import random
|
||||
import time
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
chatbot = gr.Chatbot()
|
||||
msg = gr.Textbox()
|
||||
clear = gr.Button("Clear")
|
||||
|
||||
def user(user_message, history: list):
|
||||
return "", history + [{"role": "user", "content": user_message}]
|
||||
|
||||
def bot(history: list):
|
||||
bot_message = random.choice(["How are you?", "I love you", "I'm very hungry"])
|
||||
history.append({"role": "assistant", "content": ""})
|
||||
for character in bot_message:
|
||||
history[-1]['content'] += character
|
||||
time.sleep(0.05)
|
||||
yield history
|
||||
|
||||
msg.submit(user, [msg, chatbot], [msg, chatbot], queue=False).then(
|
||||
bot, chatbot, chatbot
|
||||
)
|
||||
clear.click(lambda: None, None, chatbot, queue=False)
|
||||
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
## Custom Css
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
with gr.Column(elem_classes="cool-col"):
|
||||
gr.Markdown("### Gradio Demo with Custom CSS", elem_classes="darktest")
|
||||
gr.Markdown(
|
||||
elem_classes="markdown",
|
||||
value="Resize the browser window to see the CSS media query in action.",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
demo.launch(css_paths=["demo/custom_css/custom_css.css"])
|
||||
```
|
||||
|
||||
## Fake Diffusion
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
import numpy as np
|
||||
import time
|
||||
|
||||
def fake_diffusion(steps):
|
||||
rng = np.random.default_rng()
|
||||
for i in range(steps):
|
||||
time.sleep(1)
|
||||
image = rng.random(size=(600, 600, 3))
|
||||
yield image
|
||||
image = np.ones((1000,1000,3), np.uint8)
|
||||
image[:] = [255, 124, 0]
|
||||
yield image
|
||||
|
||||
demo = gr.Interface(fake_diffusion,
|
||||
inputs=gr.Slider(1, 10, 3, step=1),
|
||||
outputs="image",
|
||||
api_name="predict")
|
||||
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
## Hello World
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
|
||||
|
||||
def greet(name):
|
||||
return "Hello " + name + "!"
|
||||
|
||||
|
||||
demo = gr.Interface(fn=greet, inputs="textbox", outputs="textbox", api_name="predict")
|
||||
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
## Image Editor
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
import time
|
||||
|
||||
|
||||
def sleep(im):
|
||||
time.sleep(5)
|
||||
return [im["background"], im["layers"][0], im["layers"][1], im["composite"]]
|
||||
|
||||
|
||||
def predict(im):
|
||||
return im["composite"]
|
||||
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
with gr.Row():
|
||||
im = gr.ImageEditor(
|
||||
type="numpy",
|
||||
)
|
||||
im_preview = gr.Image()
|
||||
n_upload = gr.Number(0, label="Number of upload events", step=1)
|
||||
n_change = gr.Number(0, label="Number of change events", step=1)
|
||||
n_input = gr.Number(0, label="Number of input events", step=1)
|
||||
|
||||
im.upload(lambda x: x + 1, outputs=n_upload, inputs=n_upload)
|
||||
im.change(lambda x: x + 1, outputs=n_change, inputs=n_change)
|
||||
im.input(lambda x: x + 1, outputs=n_input, inputs=n_input)
|
||||
im.change(predict, outputs=im_preview, inputs=im, show_progress="hidden")
|
||||
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
## On Listener Decorator
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
name = gr.Textbox(label="Name")
|
||||
output = gr.Textbox(label="Output Box")
|
||||
greet_btn = gr.Button("Greet")
|
||||
|
||||
@gr.on(triggers=[name.submit, greet_btn.click], inputs=name, outputs=output)
|
||||
def greet(name):
|
||||
return "Hello " + name + "!"
|
||||
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
## Render Merge
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
import time
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
text_count = gr.Slider(1, 5, value=1, step=1, label="Textbox Count")
|
||||
|
||||
@gr.render(inputs=text_count)
|
||||
def render_count(count):
|
||||
boxes = []
|
||||
for i in range(count):
|
||||
box = gr.Textbox(label=f"Box {i}")
|
||||
boxes.append(box)
|
||||
|
||||
def merge(*args):
|
||||
time.sleep(0.2) # simulate a delay
|
||||
return " ".join(args)
|
||||
|
||||
merge_btn.click(merge, boxes, output)
|
||||
|
||||
def clear():
|
||||
time.sleep(0.2) # simulate a delay
|
||||
return [" "] * count
|
||||
|
||||
clear_btn.click(clear, None, boxes)
|
||||
|
||||
def countup():
|
||||
time.sleep(0.2) # simulate a delay
|
||||
return list(range(count))
|
||||
|
||||
count_btn.click(countup, None, boxes, queue=False)
|
||||
|
||||
with gr.Row():
|
||||
merge_btn = gr.Button("Merge")
|
||||
clear_btn = gr.Button("Clear")
|
||||
count_btn = gr.Button("Count")
|
||||
|
||||
output = gr.Textbox()
|
||||
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
## Reverse Audio 2
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
import numpy as np
|
||||
|
||||
def reverse_audio(audio):
|
||||
sr, data = audio
|
||||
return (sr, np.flipud(data))
|
||||
|
||||
demo = gr.Interface(fn=reverse_audio,
|
||||
inputs="microphone",
|
||||
outputs="audio", api_name="predict")
|
||||
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
## Sepia Filter
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import gradio as gr
|
||||
|
||||
def sepia(input_img):
|
||||
sepia_filter = np.array([
|
||||
[0.393, 0.769, 0.189],
|
||||
[0.349, 0.686, 0.168],
|
||||
[0.272, 0.534, 0.131]
|
||||
])
|
||||
sepia_img = input_img.dot(sepia_filter.T)
|
||||
sepia_img /= sepia_img.max()
|
||||
return sepia_img
|
||||
|
||||
demo = gr.Interface(sepia, gr.Image(), "image", api_name="predict")
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
## Sort Records
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
|
||||
def sort_records(records):
|
||||
return records.sort("Quantity")
|
||||
|
||||
demo = gr.Interface(
|
||||
sort_records,
|
||||
gr.Dataframe(
|
||||
headers=["Item", "Quantity"],
|
||||
datatype=["str", "number"],
|
||||
row_count=3,
|
||||
column_count=2,
|
||||
column_limits=(2, 2),
|
||||
type="polars"
|
||||
),
|
||||
"dataframe",
|
||||
description="Sort by Quantity"
|
||||
)
|
||||
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
## Streaming Simple
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
with gr.Row():
|
||||
with gr.Column():
|
||||
input_img = gr.Image(label="Input", sources="webcam")
|
||||
with gr.Column():
|
||||
output_img = gr.Image(label="Output")
|
||||
input_img.stream(lambda s: s, input_img, output_img, time_limit=15, stream_every=0.1, concurrency_limit=30)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
## Tabbed Interface Lite
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
|
||||
hello_world = gr.Interface(lambda name: "Hello " + name, "text", "text", api_name="predict")
|
||||
bye_world = gr.Interface(lambda name: "Bye " + name, "text", "text", api_name="predict")
|
||||
chat = gr.ChatInterface(lambda *args: "Hello " + args[0], api_name="chat")
|
||||
|
||||
demo = gr.TabbedInterface([hello_world, bye_world, chat], ["Hello World", "Bye World", "Chat"])
|
||||
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
## Tax Calculator
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
|
||||
def tax_calculator(income, marital_status, assets):
|
||||
tax_brackets = [(10, 0), (25, 8), (60, 12), (120, 20), (250, 30)]
|
||||
total_deductible = sum(cost for cost, deductible in zip(assets["Cost"], assets["Deductible"]) if deductible)
|
||||
taxable_income = income - total_deductible
|
||||
|
||||
total_tax = 0
|
||||
for bracket, rate in tax_brackets:
|
||||
if taxable_income > bracket:
|
||||
total_tax += (taxable_income - bracket) * rate / 100
|
||||
|
||||
if marital_status == "Married":
|
||||
total_tax *= 0.75
|
||||
elif marital_status == "Divorced":
|
||||
total_tax *= 0.8
|
||||
|
||||
return round(total_tax)
|
||||
|
||||
demo = gr.Interface(
|
||||
tax_calculator,
|
||||
[
|
||||
"number",
|
||||
gr.Radio(["Single", "Married", "Divorced"]),
|
||||
gr.Dataframe(
|
||||
headers=["Item", "Cost", "Deductible"],
|
||||
datatype=["str", "number", "bool"],
|
||||
label="Assets Purchased this Year",
|
||||
),
|
||||
],
|
||||
gr.Number(label="Tax due"),
|
||||
examples=[
|
||||
[10000, "Married", [["Suit", 5000, True], ["Laptop (for work)", 800, False], ["Car", 1800, True]]],
|
||||
[80000, "Single", [["Suit", 800, True], ["Watch", 1800, True], ["Food", 800, True]]],
|
||||
],
|
||||
live=True,
|
||||
api_name="predict"
|
||||
)
|
||||
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
## Timer Simple
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
import random
|
||||
import time
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
timer = gr.Timer(1)
|
||||
timestamp = gr.Number(label="Time")
|
||||
timer.tick(lambda: round(time.time()), outputs=timestamp, api_name="timestamp")
|
||||
|
||||
number = gr.Number(lambda: random.randint(1, 10), every=timer, label="Random Number")
|
||||
with gr.Row():
|
||||
gr.Button("Start").click(lambda: gr.Timer(active=True), None, timer)
|
||||
gr.Button("Stop").click(lambda: gr.Timer(active=False), None, timer)
|
||||
gr.Button("Go Fast").click(lambda: 0.2, None, timer)
|
||||
|
||||
if __name__ == "__main__":
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
## Variable Outputs
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
|
||||
max_textboxes = 10
|
||||
|
||||
def variable_outputs(k):
|
||||
k = int(k)
|
||||
return [gr.Textbox(visible=True)]*k + [gr.Textbox(visible=False)]*(max_textboxes-k)
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
s = gr.Slider(1, max_textboxes, value=max_textboxes, step=1, label="How many textboxes to show:")
|
||||
textboxes = []
|
||||
for i in range(max_textboxes):
|
||||
t = gr.Textbox(f"Textbox {i}")
|
||||
textboxes.append(t)
|
||||
|
||||
s.change(variable_outputs, s, textboxes)
|
||||
|
||||
if __name__ == "__main__":
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
## Video Identity
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
from gradio.media import get_video
|
||||
|
||||
def video_identity(video):
|
||||
return video
|
||||
|
||||
# get_video() returns file paths to sample media included with Gradio
|
||||
demo = gr.Interface(video_identity,
|
||||
gr.Video(),
|
||||
"playable_video",
|
||||
examples=[
|
||||
get_video("world.mp4")
|
||||
],
|
||||
cache_examples=True,
|
||||
api_name="predict",)
|
||||
|
||||
demo.launch()
|
||||
```
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
name: hf-gradio
|
||||
description: Use Gradio applications via API. Use when the user asks for to generate a prediction from a Gradio app on Hugging Face spaces or public URL. For example, "Generate an image using black-forest-labs/FLUX.2-dev".
|
||||
---
|
||||
|
||||
## hf-gradio CLI Skill
|
||||
|
||||
The `hf-gradio` CLI `gradio` CLI includes `info` and `predict` commands for interacting with Gradio apps programmatically.
|
||||
|
||||
## Step 1 - Verify installation
|
||||
|
||||
Verify that either `hf-gradio` or `gradio` are installed in the current virtual environment.
|
||||
|
||||
If the `hf` CLI app is installed. The `hf-gradio` extension can be installed via
|
||||
|
||||
```bash
|
||||
hf extensions install gradio-app/hf-gradio
|
||||
```
|
||||
|
||||
### Step 2 - Use `info` to discover endpoints and payload format
|
||||
|
||||
```bash
|
||||
gradio info <space_id_or_url>
|
||||
```
|
||||
|
||||
```bash
|
||||
hf-gradio info <space_id_or_url>
|
||||
```
|
||||
|
||||
```bash
|
||||
hf gradio info <space_id_or_url>
|
||||
```
|
||||
|
||||
Returns a JSON payload describing all endpoints, their parameters (with types and defaults), and return values.
|
||||
|
||||
```bash
|
||||
gradio info gradio/calculator
|
||||
# {
|
||||
# "/predict": {
|
||||
# "parameters": [
|
||||
# {"name": "num1", "required": true, "default": null, "type": {"type": "number"}},
|
||||
# {"name": "operation", "required": true, "default": null, "type": {"enum": ["add", "subtract", "multiply", "divide"], "type": "string"}},
|
||||
# {"name": "num2", "required": true, "default": null, "type": {"type": "number"}}
|
||||
# ],
|
||||
# "returns": [{"name": "output", "type": {"type": "number"}}],
|
||||
# "description": ""
|
||||
# }
|
||||
# }
|
||||
```
|
||||
|
||||
File-type parameters show `"type": "filepath"` with instructions to include `"meta": {"_type": "gradio.FileData"}` — this signals the file will be uploaded to the remote server.
|
||||
|
||||
## Step 3 - Use `predict` to generate the prediction
|
||||
|
||||
```bash
|
||||
gradio predict <space_id_or_url> <endpoint> <json_payload>
|
||||
```
|
||||
|
||||
```bash
|
||||
hf-gradio predict <space_id_or_url> <endpoint> <json_payload>
|
||||
```
|
||||
|
||||
```bash
|
||||
hf gradio predict <space_id_or_url> <endpoint> <json_payload>
|
||||
```
|
||||
|
||||
Returns a JSON object with named output keys.
|
||||
|
||||
```bash
|
||||
# Simple numeric prediction
|
||||
gradio predict gradio/calculator /predict '{"num1": 5, "operation": "multiply", "num2": 3}'
|
||||
# {"output": 15}
|
||||
|
||||
# Image generation
|
||||
gradio predict black-forest-labs/FLUX.2-dev /infer '{"prompt": "A majestic dragon"}'
|
||||
# {"Result": "/tmp/gradio/.../image.webp", "Seed": 1117868604}
|
||||
|
||||
# File upload (must include meta key)
|
||||
gradio predict gradio/image_mod /predict '{"image": {"path": "/path/to/image.png", "meta": {"_type": "gradio.FileData"}}}'
|
||||
# {"output": "/tmp/gradio/.../output.png"}
|
||||
```
|
||||
|
||||
Both commands accept `--token` for accessing private Spaces.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
@@ -0,0 +1,332 @@
|
||||
const { getPackagesSync } = require("@manypkg/get-packages");
|
||||
const dependents_graph = require("@changesets/get-dependents-graph");
|
||||
|
||||
const gh = require("@changesets/get-github-info");
|
||||
const { existsSync, readFileSync, writeFileSync } = require("fs");
|
||||
const { join } = require("path");
|
||||
|
||||
const { getInfo, getInfoFromPullRequest } = gh;
|
||||
const pkg_data = getPackagesSync(process.cwd());
|
||||
const { packages, rootDir } = pkg_data;
|
||||
const dependents = dependents_graph.getDependentsGraph({
|
||||
packages,
|
||||
root: pkg_data.rootPackage
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {{packageJson: {name: string, python?: boolean}, dir: string}} Package
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{summary: string, id: string, commit: string, releases: {name: string}}} Changeset
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} package_name The name of the package to find the directories for
|
||||
* @returns {string[]} The directories for the package
|
||||
*/
|
||||
function find_packages_dirs(package_name) {
|
||||
/** @type {string[]} */
|
||||
let package_dirs = [];
|
||||
|
||||
/** @type {Package | undefined} */
|
||||
const _package = packages.find((p) => p.packageJson.name === package_name);
|
||||
if (!_package) throw new Error(`Package ${package_name} not found`);
|
||||
|
||||
package_dirs.push(_package.dir);
|
||||
if (_package.packageJson.python) {
|
||||
package_dirs.push(join(_package.dir, ".."));
|
||||
}
|
||||
return package_dirs;
|
||||
}
|
||||
|
||||
let lines = {
|
||||
_handled: []
|
||||
};
|
||||
|
||||
const changelogFunctions = {
|
||||
/**
|
||||
*
|
||||
* @param {Changeset[]} changesets The changesets that have been created
|
||||
* @param {any} dependenciesUpdated The dependencies that have been updated
|
||||
* @param {any} options The options passed to the changelog generator
|
||||
* @returns {Promise<string>} The release line for the dependencies
|
||||
*/
|
||||
getDependencyReleaseLine: async (
|
||||
changesets,
|
||||
dependenciesUpdated,
|
||||
options
|
||||
) => {
|
||||
if (!options.repo) {
|
||||
throw new Error(
|
||||
'Please provide a repo to this changelog generator like this:\n"changelog": ["@changesets/changelog-github", { "repo": "org/repo" }]'
|
||||
);
|
||||
}
|
||||
if (dependenciesUpdated.length === 0) return "";
|
||||
|
||||
const changesetLink = `- Updated dependencies [${(
|
||||
await Promise.all(
|
||||
changesets.map(async (cs) => {
|
||||
if (cs.commit) {
|
||||
let { links } = await getInfo({
|
||||
repo: options.repo,
|
||||
commit: cs.commit
|
||||
});
|
||||
return links.commit;
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
.filter((_) => _)
|
||||
.join(", ")}]:`;
|
||||
|
||||
const updatedDepenenciesList = dependenciesUpdated.map(
|
||||
/**
|
||||
*
|
||||
* @param {any} dependency The dependency that has been updated
|
||||
* @returns {string} The formatted dependency
|
||||
*/
|
||||
(dependency) => {
|
||||
const updates = dependents.get(dependency.name);
|
||||
|
||||
if (updates && updates.length > 0) {
|
||||
updates.forEach((update) => {
|
||||
if (!lines[update]) {
|
||||
lines[update] = {
|
||||
dirs: find_packages_dirs(update),
|
||||
current_changelog: "",
|
||||
feat: [],
|
||||
fix: [],
|
||||
highlight: [],
|
||||
previous_version: packages.find(
|
||||
(p) => p.packageJson.name === update
|
||||
).packageJson.version,
|
||||
dependencies: []
|
||||
};
|
||||
|
||||
const changelog_path = join(
|
||||
//@ts-ignore
|
||||
lines[update].dirs[1] || lines[update].dirs[0],
|
||||
"CHANGELOG.md"
|
||||
);
|
||||
|
||||
if (existsSync(changelog_path)) {
|
||||
//@ts-ignore
|
||||
lines[update].current_changelog = readFileSync(
|
||||
changelog_path,
|
||||
"utf-8"
|
||||
)
|
||||
.replace(`# ${update}`, "")
|
||||
.trim();
|
||||
}
|
||||
}
|
||||
lines[update].dependencies.push(
|
||||
` - ${dependency.name}@${dependency.newVersion}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return ` - ${dependency.name}@${dependency.newVersion}`;
|
||||
}
|
||||
);
|
||||
|
||||
writeFileSync(
|
||||
join(rootDir, ".changeset", "_changelog.json"),
|
||||
JSON.stringify(lines, null, 2)
|
||||
);
|
||||
|
||||
return [changesetLink, ...updatedDepenenciesList].join("\n");
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {{summary: string, id: string, commit: string, releases: {name: string}[]}} changeset The changeset that has been created
|
||||
* @param {any} type The type of changeset
|
||||
* @param {any} options The options passed to the changelog generator
|
||||
* @returns {Promise<string>} The release line for the changeset
|
||||
*/
|
||||
getReleaseLine: async (changeset, type, options) => {
|
||||
if (!options || !options.repo) {
|
||||
throw new Error(
|
||||
'Please provide a repo to this changelog generator like this:\n"changelog": ["@changesets/changelog-github", { "repo": "org/repo" }]'
|
||||
);
|
||||
}
|
||||
|
||||
let prFromSummary;
|
||||
let commitFromSummary;
|
||||
/**
|
||||
* @type {string[]}
|
||||
*/
|
||||
let usersFromSummary = [];
|
||||
|
||||
const replacedChangelog = changeset.summary
|
||||
.replace(/^\s*(?:pr|pull|pull\s+request):\s*#?(\d+)/im, (_, pr) => {
|
||||
let num = Number(pr);
|
||||
if (!isNaN(num)) prFromSummary = num;
|
||||
return "";
|
||||
})
|
||||
.replace(/^\s*commit:\s*([^\s]+)/im, (_, commit) => {
|
||||
commitFromSummary = commit;
|
||||
return "";
|
||||
})
|
||||
.replace(/^\s*(?:author|user):\s*@?([^\s]+)/gim, (_, user) => {
|
||||
usersFromSummary.push(user);
|
||||
return "";
|
||||
})
|
||||
.trim();
|
||||
|
||||
const [firstLine, ...futureLines] = replacedChangelog
|
||||
.split("\n")
|
||||
.map((l) => l.trimRight());
|
||||
|
||||
const links = await (async () => {
|
||||
if (prFromSummary !== undefined) {
|
||||
let { links } = await getInfoFromPullRequest({
|
||||
repo: options.repo,
|
||||
pull: prFromSummary
|
||||
});
|
||||
if (commitFromSummary) {
|
||||
links = {
|
||||
...links,
|
||||
commit: `[\`${commitFromSummary}\`](https://github.com/${options.repo}/commit/${commitFromSummary})`
|
||||
};
|
||||
}
|
||||
return links;
|
||||
}
|
||||
const commitToFetchFrom = commitFromSummary || changeset.commit;
|
||||
if (commitToFetchFrom) {
|
||||
let { links } = await getInfo({
|
||||
repo: options.repo,
|
||||
commit: commitToFetchFrom
|
||||
});
|
||||
return links;
|
||||
}
|
||||
return {
|
||||
commit: null,
|
||||
pull: null,
|
||||
user: null
|
||||
};
|
||||
})();
|
||||
|
||||
const user_link = /\[(@[^]+)\]/.exec(links.user);
|
||||
const users =
|
||||
usersFromSummary && usersFromSummary.length
|
||||
? usersFromSummary
|
||||
.map((userFromSummary) => `@${userFromSummary}`)
|
||||
.join(", ")
|
||||
: user_link
|
||||
? user_link[1]
|
||||
: links.user;
|
||||
|
||||
const prefix = [
|
||||
links.pull === null ? "" : `${links.pull}`,
|
||||
links.commit === null ? "" : `${links.commit}`
|
||||
]
|
||||
.join(" ")
|
||||
.trim();
|
||||
|
||||
const suffix = users === null ? "" : ` Thanks ${users}!`;
|
||||
|
||||
/**
|
||||
* @typedef {{[key: string]: string[] | {dirs: string[], current_changelog: string, feat: {summary: string}[], fix: {summary: string}[], highlight: {summary: string}[]}}} ChangesetMeta
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type { ChangesetMeta & { _handled: string[] } }}
|
||||
*/
|
||||
|
||||
if (lines._handled.includes(changeset.id)) {
|
||||
return "done";
|
||||
}
|
||||
lines._handled.push(changeset.id);
|
||||
|
||||
changeset.releases.forEach((release) => {
|
||||
if (!lines[release.name]) {
|
||||
lines[release.name] = {
|
||||
dirs: find_packages_dirs(release.name),
|
||||
current_changelog: "",
|
||||
feat: [],
|
||||
fix: [],
|
||||
highlight: [],
|
||||
previous_version: packages.find(
|
||||
(p) => p.packageJson.name === release.name
|
||||
).packageJson.version,
|
||||
dependencies: []
|
||||
};
|
||||
}
|
||||
|
||||
const changelog_path = join(
|
||||
//@ts-ignore
|
||||
lines[release.name].dirs[1] || lines[release.name].dirs[0],
|
||||
"CHANGELOG.md"
|
||||
);
|
||||
|
||||
if (existsSync(changelog_path)) {
|
||||
//@ts-ignore
|
||||
lines[release.name].current_changelog = readFileSync(
|
||||
changelog_path,
|
||||
"utf-8"
|
||||
)
|
||||
.replace(`# ${release.name}`, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
const [, _type, summary] = changeset.summary
|
||||
.trim()
|
||||
.match(/^(feat|fix|highlight)\s*:\s*([^]*)/im) || [
|
||||
,
|
||||
"feat",
|
||||
changeset.summary
|
||||
];
|
||||
|
||||
let formatted_summary = "";
|
||||
|
||||
if (_type === "highlight") {
|
||||
const [heading, ...rest] = summary.trim().split("\n");
|
||||
const _heading = `${heading} ${prefix ? `(${prefix})` : ""}`;
|
||||
const _rest = rest.concat(["", suffix]);
|
||||
|
||||
formatted_summary = `${_heading}\n${_rest.join("\n")}`;
|
||||
} else {
|
||||
formatted_summary = handle_line(summary, prefix, suffix);
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
lines[release.name][_type].push({
|
||||
summary: formatted_summary
|
||||
});
|
||||
});
|
||||
|
||||
writeFileSync(
|
||||
join(rootDir, ".changeset", "_changelog.json"),
|
||||
JSON.stringify(lines, null, 2)
|
||||
);
|
||||
|
||||
return `\n\n-${prefix ? `${prefix} -` : ""} ${firstLine}\n${futureLines
|
||||
.map((l) => ` ${l}`)
|
||||
.join("\n")}`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} str The changelog entry
|
||||
* @param {string} prefix The prefix to add to the first line
|
||||
* @param {string} suffix The suffix to add to the last line
|
||||
* @returns {string} The formatted changelog entry
|
||||
*/
|
||||
function handle_line(str, prefix, suffix) {
|
||||
const [_s, ...lines] = str.split("\n").filter(Boolean);
|
||||
|
||||
const desc = `${prefix ? `${prefix} -` : ""} ${_s.replace(
|
||||
/[\s\.]$/,
|
||||
""
|
||||
)}. ${suffix}`;
|
||||
|
||||
if (_s.length === 1) {
|
||||
return desc;
|
||||
}
|
||||
|
||||
return [desc, ...lines.map((l) => ` ${l}`)].join("/n");
|
||||
}
|
||||
|
||||
module.exports = changelogFunctions;
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
|
||||
"changelog": ["./changeset.cjs", { "repo": "gradio-app/gradio" }],
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["@self/spaces-test", "@self/cdn-test"]
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
const { join } = require("path");
|
||||
const { readFileSync, existsSync, writeFileSync, unlinkSync } = require("fs");
|
||||
const { getPackagesSync } = require("@manypkg/get-packages");
|
||||
|
||||
const RE_PKG_NAME = /^[\w-]+\b/;
|
||||
const pkg_meta = getPackagesSync(process.cwd());
|
||||
|
||||
/**
|
||||
* @typedef {{dirs: string[], highlight: {summary: string}[], feat: {summary: string}[], fix: {summary: string}[], current_changelog: string}} ChangesetMeta
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{[key: string]: ChangesetMeta}} ChangesetMetaCollection
|
||||
*/
|
||||
|
||||
function run() {
|
||||
if (!existsSync(join(pkg_meta.rootDir, ".changeset", "_changelog.json"))) {
|
||||
console.warn("No changesets to process");
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type { ChangesetMetaCollection & { _handled: string[] } }}
|
||||
*/
|
||||
const { _handled, ...packages } = JSON.parse(
|
||||
readFileSync(
|
||||
join(pkg_meta.rootDir, ".changeset", "_changelog.json"),
|
||||
"utf-8"
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* @typedef { {packageJson: {name: string, version: string, python: boolean}, dir: string} } PackageMeta
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type { {[key:string]: PackageMeta} }
|
||||
*/
|
||||
const all_packages = pkg_meta.packages.reduce((acc, pkg) => {
|
||||
acc[pkg.packageJson.name] = /**@type {PackageMeta} */ (
|
||||
/** @type {unknown} */ (pkg)
|
||||
);
|
||||
return acc;
|
||||
}, /** @type {{[key:string] : PackageMeta}} */ ({}));
|
||||
|
||||
for (const pkg_name in packages) {
|
||||
const { dirs, highlight, feat, fix, current_changelog, dependencies } =
|
||||
/**@type {ChangesetMeta} */ (packages[pkg_name]);
|
||||
|
||||
if (pkg_name === "@gradio/lite") {
|
||||
const target = all_packages.gradio.packageJson.version.split(".");
|
||||
|
||||
const current_version = packages[pkg_name].previous_version.split(".");
|
||||
|
||||
if (!packages.gradio) {
|
||||
const patch = parseInt(current_version[2]) + 1;
|
||||
const new_version = [target[0], target[1], patch];
|
||||
all_packages[pkg_name].packageJson.version = new_version.join(".");
|
||||
} else {
|
||||
if (parseInt(target[1]) > parseInt(current_version[1])) {
|
||||
all_packages[pkg_name].packageJson.version = target.join(".");
|
||||
} else if (parseInt(target[1]) === parseInt(current_version[1])) {
|
||||
const patch = parseInt(current_version[2]) + 1;
|
||||
const new_version = [target[0], target[1], patch];
|
||||
all_packages[pkg_name].packageJson.version = new_version.join(".");
|
||||
}
|
||||
}
|
||||
|
||||
writeFileSync(
|
||||
join(all_packages[pkg_name].dir, "package.json"),
|
||||
JSON.stringify(all_packages[pkg_name].packageJson, null, "\t") + "\n"
|
||||
);
|
||||
}
|
||||
|
||||
const { version, python } = all_packages[pkg_name].packageJson;
|
||||
|
||||
const highlights = highlight?.map((h) => `${h.summary}`) || [];
|
||||
const features = feat?.map((f) => `- ${f.summary}`) || [];
|
||||
const fixes = fix?.map((f) => `- ${f.summary}`) || [];
|
||||
const deps = Array.from(new Set(dependencies?.map((d) => d.trim()))) || [];
|
||||
|
||||
const release_notes = /** @type {[string[], string][]} */ ([
|
||||
[highlights, "### Highlights"],
|
||||
[features, "### Features"],
|
||||
[fixes, "### Fixes"],
|
||||
[deps, "### Dependency updates"]
|
||||
])
|
||||
.filter(([s], i) => s.length > 0)
|
||||
.map(([lines, title]) => {
|
||||
if (title === "### Highlights") {
|
||||
return `${title}\n\n${lines.join("\n\n")}`;
|
||||
}
|
||||
|
||||
return `${title}\n\n${lines.join("\n")}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
|
||||
const new_changelog = `# ${pkg_name}
|
||||
|
||||
## ${version}
|
||||
|
||||
${release_notes}
|
||||
|
||||
${current_changelog.replace(`# ${pkg_name}`, "").trim()}
|
||||
`.trim();
|
||||
|
||||
dirs.forEach((dir) => {
|
||||
writeFileSync(join(dir, "CHANGELOG.md"), new_changelog);
|
||||
});
|
||||
|
||||
if (python) {
|
||||
bump_local_dependents(pkg_name, version);
|
||||
}
|
||||
}
|
||||
|
||||
unlinkSync(join(pkg_meta.rootDir, ".changeset", "_changelog.json"));
|
||||
|
||||
/**
|
||||
* @param {string} pkg_to_bump The name of the package to bump
|
||||
* @param {string} version The version to bump to
|
||||
* @returns {void}
|
||||
* */
|
||||
function bump_local_dependents(pkg_to_bump, version) {
|
||||
for (const pkg_name in all_packages) {
|
||||
const {
|
||||
dir,
|
||||
packageJson: { python }
|
||||
} = all_packages[pkg_name];
|
||||
|
||||
if (!python) continue;
|
||||
|
||||
const requirements_path = join(dir, "..", "requirements.txt");
|
||||
const requirements = readFileSync(requirements_path, "utf-8").split("\n");
|
||||
|
||||
const pkg_index = requirements.findIndex((line) => {
|
||||
const m = line.trim().match(RE_PKG_NAME);
|
||||
if (!m) return false;
|
||||
return m[0] === pkg_to_bump;
|
||||
});
|
||||
|
||||
if (pkg_index !== -1) {
|
||||
requirements[pkg_index] = `${pkg_to_bump}==${version}`;
|
||||
writeFileSync(requirements_path, requirements.join("\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gradio": minor
|
||||
---
|
||||
|
||||
feat:Fix spurious separators from empty tokens in `HighlightedText` with `combine_adjacent=True`
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@gradio/code": patch
|
||||
"gradio": patch
|
||||
---
|
||||
|
||||
fix:Fix `gr.Code` not rendering when it has no initial value
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gradio": patch
|
||||
---
|
||||
|
||||
fix:Fix `gr.State` passing its callable default to event handlers instead of the called value
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../.agents/skills
|
||||
@@ -0,0 +1,35 @@
|
||||
**/*.md
|
||||
**/js/app/public/**
|
||||
**/pnpm-workspace.yaml
|
||||
**/js/app/dist/**
|
||||
**/js/wasm/dist/**
|
||||
**/js/preview/dist/**
|
||||
**/client/js/dist/**
|
||||
**/js/*/dist/**
|
||||
**/pnpm-lock.yaml
|
||||
**/js/plot/src/Plot.svelte
|
||||
**/.svelte-kit/**
|
||||
**/demo/**
|
||||
**/gradio/**
|
||||
**/.pnpm-store/**
|
||||
**/.venv/**
|
||||
|
||||
/guides/**
|
||||
**/.mypy_cache/**
|
||||
!test-strategy.md
|
||||
**/js/_space-test/**
|
||||
../js/lite/src/theme.css
|
||||
../js/storybook/theme.css
|
||||
**/gradio_cached_examples/**
|
||||
**/storybook-static/**
|
||||
**/.vscode/**
|
||||
sweep.yaml
|
||||
**/.vercel/**
|
||||
**/src/lib/json/**/*
|
||||
**/playwright/.cache/**/*
|
||||
**/theme/src/pollen.css
|
||||
**/venv/**
|
||||
../js/app/src/api_docs/CodeSnippet.svelte
|
||||
../js/app/src/api_docs/RecordingSnippet.svelte
|
||||
../.changeset/pre.json
|
||||
../js/_website/build/**/*
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 80,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
[
|
||||
"audio_debugger",
|
||||
"blocks_essay",
|
||||
"blocks_group",
|
||||
"blocks_js_methods",
|
||||
"blocks_layout",
|
||||
"blocks_multiple_event_triggers",
|
||||
"blocks_update",
|
||||
"calculator",
|
||||
"cancel_events",
|
||||
"chatbot_multimodal",
|
||||
"chatinterface_streaming_echo",
|
||||
"clear_components",
|
||||
"code",
|
||||
"diff_texts",
|
||||
"fake_gan",
|
||||
"fake_diffusion_with_gif",
|
||||
"file_explorer_component_events",
|
||||
"gradio_pdf_demo",
|
||||
"iframe_resizer",
|
||||
"image_mod_default_image",
|
||||
"image_editor_events",
|
||||
"image_segmentation",
|
||||
"interface_random_slider",
|
||||
"kitchen_sink",
|
||||
"kitchen_sink_random",
|
||||
"login_with_huggingface",
|
||||
"matrix_transpose",
|
||||
"mini_leaderboard",
|
||||
"model3D",
|
||||
"native_plots",
|
||||
"reverse_audio",
|
||||
"stt_or_tts",
|
||||
"stream_audio",
|
||||
"stream_audio_out",
|
||||
"stream_frames",
|
||||
"video_component",
|
||||
"zip_files"
|
||||
]
|
||||
@@ -0,0 +1,157 @@
|
||||
import globals from "globals";
|
||||
import ts_plugin from "@typescript-eslint/eslint-plugin";
|
||||
import js_plugin from "@eslint/js";
|
||||
import jsdoc from "eslint-plugin-jsdoc";
|
||||
|
||||
import typescriptParser from "@typescript-eslint/parser";
|
||||
import sveltePlugin from "eslint-plugin-svelte";
|
||||
import svelteParser from "svelte-eslint-parser";
|
||||
|
||||
const ts_rules_disabled = Object.fromEntries(
|
||||
Object.keys(ts_plugin.rules).map((rule) => [
|
||||
`@typescript-eslint/${rule}`,
|
||||
"off"
|
||||
])
|
||||
);
|
||||
const js_rules_disabled = Object.fromEntries(
|
||||
Object.keys(js_plugin.configs.all.rules).map((rule) => [rule, "off"])
|
||||
);
|
||||
|
||||
const jsdoc_rules_disabled = Object.fromEntries(
|
||||
Object.keys(jsdoc.configs.recommended.rules).map((rule) => [
|
||||
`jsdoc/${rule}`,
|
||||
"off"
|
||||
])
|
||||
);
|
||||
|
||||
const js_rules = {
|
||||
...js_rules_disabled,
|
||||
// "no-console": ["error", { allow: ["warn", "error", "debug", "info"] }],
|
||||
"no-constant-condition": "error",
|
||||
"no-dupe-args": "error",
|
||||
"no-extra-boolean-cast": "error",
|
||||
"no-unexpected-multiline": "error",
|
||||
"no-unreachable": "error",
|
||||
"array-callback-return": "error",
|
||||
"no-undef": "error"
|
||||
};
|
||||
|
||||
const ts_rules = {
|
||||
...ts_rules_disabled,
|
||||
"@typescript-eslint/adjacent-overload-signatures": "error",
|
||||
"@typescript-eslint/consistent-type-exports": "error",
|
||||
"@typescript-eslint/array-type": "error",
|
||||
"@typescript-eslint/no-inferrable-types": "error"
|
||||
};
|
||||
|
||||
const jsdoc_rules = {
|
||||
...jsdoc_rules_disabled
|
||||
};
|
||||
|
||||
const { browser, es2021, node } = globals;
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: [
|
||||
"**/.svelte-kit/**/*",
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/.config/*",
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.ts",
|
||||
"**/*.node-test.ts",
|
||||
"**/*.stories.*",
|
||||
"client/js/src/test/**/*",
|
||||
"js/build/out/**/*",
|
||||
"js/spa/test/**/*",
|
||||
"**/*vite.config.ts",
|
||||
"**/_website/**/*",
|
||||
"**/app/**/*",
|
||||
"**/_spaces-test/**/*",
|
||||
"**/preview/test/**/*",
|
||||
"**/component-test/**/*",
|
||||
"**/js/wasm/src/webworker/**/*"
|
||||
]
|
||||
},
|
||||
{
|
||||
files: ["**/*.js", "**/*.cjs"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...browser,
|
||||
...es2021,
|
||||
...node
|
||||
}
|
||||
},
|
||||
|
||||
plugins: {
|
||||
"eslint:recommended": js_plugin,
|
||||
jsdoc
|
||||
},
|
||||
rules: { ...js_rules, ...jsdoc_rules }
|
||||
},
|
||||
|
||||
{
|
||||
files: ["**/*.ts"],
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
extraFileExtensions: [".svelte"]
|
||||
},
|
||||
globals: {
|
||||
...browser,
|
||||
...es2021,
|
||||
...node
|
||||
}
|
||||
},
|
||||
|
||||
plugins: {
|
||||
"@typescript-eslint": ts_plugin,
|
||||
"eslint:recommended": js_plugin,
|
||||
jsdoc
|
||||
},
|
||||
rules: {
|
||||
...ts_rules,
|
||||
...js_rules,
|
||||
...jsdoc_rules,
|
||||
"no-undef": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["**/client/js/**"],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: "./client/js/tsconfig.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["**/*.svelte"],
|
||||
languageOptions: {
|
||||
parser: svelteParser,
|
||||
parserOptions: {
|
||||
parser: typescriptParser,
|
||||
project: "./tsconfig.json",
|
||||
extraFileExtensions: [".svelte"]
|
||||
},
|
||||
globals: {
|
||||
...browser,
|
||||
...es2021
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
svelte: sveltePlugin,
|
||||
"@typescript-eslint": ts_plugin,
|
||||
"eslint:recommended": js_plugin,
|
||||
jsdoc
|
||||
},
|
||||
rules: {
|
||||
...ts_rules,
|
||||
...js_rules,
|
||||
...jsdoc_rules,
|
||||
...sveltePlugin.configs.recommended.rules,
|
||||
"svelte/no-at-html-tags": "off",
|
||||
"no-undef": "off"
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,56 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
const base = defineConfig({
|
||||
use: {
|
||||
screenshot: "only-on-failure",
|
||||
trace: "retain-on-failure",
|
||||
bypassCSP: true,
|
||||
launchOptions: {
|
||||
args: [
|
||||
"--disable-web-security",
|
||||
"--use-fake-device-for-media-stream",
|
||||
"--use-fake-ui-for-media-stream",
|
||||
"--use-file-for-fake-audio-capture=../gradio/test_data/test_audio.wav"
|
||||
]
|
||||
}
|
||||
},
|
||||
expect: { timeout: 10_000 },
|
||||
timeout: 30_000,
|
||||
testMatch: /.*\.spec\.ts/,
|
||||
testDir: "..",
|
||||
// Reload tests must stay serial (CUSTOM_TEST=1) — several rewrite a shared
|
||||
// `run.py` in the same cwd, so parallel workers would clobber each other.
|
||||
// For the regular browser suite on CI, allow PW_BROWSER_WORKERS to raise the
|
||||
// count above the historical default of 4 (the runner has 16 cores and the
|
||||
// tests are largely wait-bound, so they parallelize well).
|
||||
workers: process.env.CUSTOM_TEST
|
||||
? 1
|
||||
: process.env.CI
|
||||
? Number(process.env.PW_BROWSER_WORKERS) || 4
|
||||
: undefined,
|
||||
retries: 3,
|
||||
fullyParallel: false
|
||||
});
|
||||
|
||||
// There are Firefox-specific issues such as https://github.com/gradio-app/gradio/pull/9528 so we want to run the tests on Firefox, but Firefox sometimes fails to start in the GitHub Actions environment so we disable it on CI.
|
||||
const localOnly = (project) => (process.env.CI ? undefined : project);
|
||||
|
||||
const normal = defineConfig(base, {
|
||||
// globalSetup removed - each test file now launches its own Gradio app via fixture
|
||||
projects: [
|
||||
localOnly({
|
||||
name: "firefox",
|
||||
use: { ...devices["Desktop Firefox"] },
|
||||
grep: /@firefox/
|
||||
}),
|
||||
{
|
||||
name: "chrome",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
permissions: ["clipboard-read", "clipboard-write", "microphone"]
|
||||
}
|
||||
}
|
||||
].filter(Boolean)
|
||||
});
|
||||
|
||||
export default normal;
|
||||
@@ -0,0 +1,8 @@
|
||||
const tailwindcss = require("tailwindcss");
|
||||
const autoprefixer = require("autoprefixer");
|
||||
const nested = require("tailwindcss/nesting");
|
||||
const tw_config = require("./tailwind.config.cjs");
|
||||
|
||||
module.exports = {
|
||||
plugins: [nested, tailwindcss(tw_config), autoprefixer]
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import subprocess
|
||||
|
||||
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
||||
|
||||
|
||||
class BuildHook(BuildHookInterface):
|
||||
def finalize(self, version, build_data, artifact_path):
|
||||
subprocess.run(["pyodide", "py-compile", "--keep", artifact_path], check=True)
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { TestingLibraryMatchers } from "@testing-library/jest-dom/matchers";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
|
||||
declare module "vitest" {
|
||||
interface Assertion<T = any>
|
||||
extends jest.Matchers<void, T>, TestingLibraryMatchers<T, void> {}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||
|
||||
export default {
|
||||
preprocess: vitePreprocess(),
|
||||
compilerOptions: {
|
||||
experimental: {
|
||||
async: true
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{html,js,svelte,ts}",
|
||||
"**/@gradio/**/*.{html,js,svelte,ts}"
|
||||
],
|
||||
|
||||
theme: {
|
||||
extend: {}
|
||||
},
|
||||
|
||||
plugins: [require("@tailwindcss/forms")]
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import config from "../js/spa/vite.config";
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,43 @@
|
||||
// See https://containers.dev
|
||||
{
|
||||
"name": "Python 3",
|
||||
"image": "mcr.microsoft.com/devcontainers/python:1-3.10",
|
||||
|
||||
// See https://containers.dev/features
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/git:1": {},
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"pnpmVersion": "9"
|
||||
},
|
||||
"ghcr.io/devcontainers-contrib/features/ffmpeg-apt-get:1": {}
|
||||
},
|
||||
|
||||
"hostRequirements": {
|
||||
"cpus": 4,
|
||||
"memory": "8gb",
|
||||
"storage": "32gb"
|
||||
},
|
||||
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
"ms-python.black-formatter",
|
||||
"ms-toolsai.jupyter",
|
||||
"esbenp.prettier-vscode",
|
||||
"svelte.svelte-vscode",
|
||||
"phoenisx.cssvar"
|
||||
],
|
||||
"remote.autoForwardPorts": false
|
||||
}
|
||||
},
|
||||
|
||||
"forwardPorts": [7860, 9876],
|
||||
"portsAttributes": {
|
||||
"7860": { "label": "gradio port" },
|
||||
"9876": { "label": "gradio dev port" }
|
||||
},
|
||||
|
||||
"postCreateCommand": "export NODE_OPTIONS=\"--max-old-space-size=8192\" && chmod +x scripts/install_gradio.sh scripts/install_test_requirements.sh scripts/build_frontend.sh && ./scripts/install_test_requirements.sh && ./scripts/install_gradio.sh && ./scripts/build_frontend.sh"
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
# Python build
|
||||
.eggs/
|
||||
gradio.egg-info/*
|
||||
!gradio.egg-info/requires.txt
|
||||
!gradio.egg-info/PKG-INFO
|
||||
dist/
|
||||
*.pyc
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
build/
|
||||
|
||||
# JS build
|
||||
gradio/templates/frontend/static
|
||||
gradio/templates/frontend/cdn
|
||||
|
||||
# Secrets
|
||||
.env
|
||||
|
||||
# Gradio run artifacts
|
||||
*.db
|
||||
*.sqlite3
|
||||
gradio/launches.json
|
||||
gradio/hash_seed.txt
|
||||
|
||||
# Tests
|
||||
.coverage
|
||||
coverage.xml
|
||||
test.txt
|
||||
|
||||
# Demos
|
||||
demo/tmp.zip
|
||||
demo/flagged
|
||||
demo/files/*.avi
|
||||
demo/files/*.mp4
|
||||
|
||||
# Etc
|
||||
.idea/*
|
||||
.DS_Store
|
||||
*.bak
|
||||
workspace.code-workspace
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
root = true
|
||||
|
||||
[{js/**,client/js/**}]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = tab
|
||||
tab_width = 2
|
||||
@@ -0,0 +1,14 @@
|
||||
# https://github.com/gradio-app/gradio/pull/4487 - refactor components.py to separate files
|
||||
69f36f98535c904e7cac2b4942cecc747ed7443c
|
||||
# Format the codebase
|
||||
cc0cff893f9d7d472788adc2510c123967b384fe
|
||||
# Switch from black to ruff
|
||||
8a70e83db9c7751b46058cdd2514e6bddeef6210
|
||||
# format (#4810)
|
||||
7fa5e766ce0f89f1fb84c329e62c9df9c332120a
|
||||
# lint website
|
||||
4bf301324b3b180fa32166ff1774312b01334c88
|
||||
# format frontend with prettier
|
||||
980b9f60eb49ed81e4957debe7b23a559a4d4b51
|
||||
# Refactor component directories (#5074)
|
||||
1419538ea795caa391e3de809379f10639e9e764
|
||||
@@ -0,0 +1,69 @@
|
||||
name: "\U0001F41E Bug report"
|
||||
description: Report a bug on Gradio
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report! Before you get started, please [search to see](https://github.com/gradio-app/gradio/issues) if an issue already exists for the bug you encountered
|
||||
- type: textarea
|
||||
id: bug-description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: Please provide a concise description of what the bug is, in clear English. If you intend to submit a PR for this issue, tell us in the description.
|
||||
placeholder: Bug description
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Have you searched existing issues? 🔎
|
||||
description: Please search to see if an issue already exists for the issue you encountered.
|
||||
options:
|
||||
- label: I have searched and found no existing issues
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Reproduction
|
||||
description: Please provide a minimal example, with code, that can be run to reproduce the issue. Do NOT provide screenshots of code, or link to external repos or applications. Use ``` to format code blocks.
|
||||
placeholder: Reproduction
|
||||
value: |
|
||||
```python
|
||||
import gradio as gr
|
||||
|
||||
```
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: screenshot
|
||||
attributes:
|
||||
label: Screenshot
|
||||
description: If relevant, please include screenshot(s) of your Gradio app so that we can understand what the issue is.
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs
|
||||
description: "Please include the full stacktrace of the errors you get from Python or Javascript. If you are running in a colab notebooks, you can get the logs with by setting `debug=True`, i.e: `gradio.Interface.launch(debug=True)`"
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: system-info
|
||||
attributes:
|
||||
label: System Info
|
||||
description: Please ensure you are running the latest version of Gradio. You can get the Gradio version and all its dependencies by running `gradio environment`
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: severity
|
||||
attributes:
|
||||
label: Severity
|
||||
description: Select the severity of this issue
|
||||
options:
|
||||
- I can work around it
|
||||
- Blocking usage of gradio
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
📌 Please ensure that you have filled all of the required sections above, and that the reproduction you have provided is [minimal, complete, and reproducible](https://stackoverflow.com/help/minimal-reproducible-example). Incomplete issues will be closed.
|
||||
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 💡 General questions
|
||||
url: https://discord.com/invite/feTf9x3ZSB
|
||||
about: Have general questions about how to use Gradio? Please ask in our community Discord for quicker responses
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: ⚡ Feature request
|
||||
about: Suggest an improvement or new feature or a new Guide for Gradio
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
- [ ] I have searched to see if a similar issue already exists.
|
||||
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,25 @@
|
||||
## Description
|
||||
|
||||
Please include a concise summary, in clear English, of the changes in this pull request. If it closes an issue, please mention it here.
|
||||
|
||||
Closes: #(issue)
|
||||
|
||||
## AI Disclosure
|
||||
|
||||
We encourage the use of AI tooling in creating PRs, but the any non-trivial use of AI needs be disclosed. E.g. if you used Claude to write a first draft, you should mention that. Trivial tab-completion doesn't need to be disclosed. You should self-review all PRs, especially if they were generated with AI.
|
||||
|
||||
- [ ] I used AI to... [fill here]
|
||||
- [ ] I did not use AI
|
||||
|
||||
## 🎯 PRs Should Target Issues
|
||||
|
||||
Before your create a PR, please check to see if there is [an existing issue](https://github.com/gradio-app/gradio/issues) for this change. If not, please create an issue before you create this PR, unless the fix is very small.
|
||||
|
||||
Not adhering to this guideline will result in the PR being closed.
|
||||
|
||||
## Testing and Formatting Your Code
|
||||
|
||||
1. PRs will only be merged if tests pass on CI. We recommend at least running the backend tests locally, please set up [your Gradio environment locally](https://github.com/gradio-app/gradio/blob/main/CONTRIBUTING.md) and run the backed tests: `bash scripts/run_backend_tests.sh`
|
||||
|
||||
2. Please run these bash scripts to automatically format your code: `bash scripts/format_backend.sh`, and (if you made any changes to non-Python files) `bash scripts/format_frontend.sh`
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
name: "prepare"
|
||||
description: "Prepare workflow"
|
||||
|
||||
inputs:
|
||||
token:
|
||||
description: "GitHub token"
|
||||
filter:
|
||||
description: "Which filter to use"
|
||||
|
||||
outputs:
|
||||
should_run:
|
||||
description: "Whether to run the workflow"
|
||||
value: ${{ steps.pr.outputs.should_run }}
|
||||
pr_number:
|
||||
description: "PR number"
|
||||
value: ${{ steps.pr.outputs.pr_number }}
|
||||
sha:
|
||||
description: "SHA of the HEAD commit of the PR"
|
||||
value: ${{ steps.pr.outputs.sha }}
|
||||
source_repo:
|
||||
description: "Source repo"
|
||||
value: ${{ steps.pr.outputs.source_repo }}
|
||||
source_branch:
|
||||
description: "Source branch"
|
||||
value: ${{ steps.pr.outputs.source_branch }}
|
||||
labels:
|
||||
description: "Labels on the PR"
|
||||
value: ${{ steps.pr.outputs.labels }}
|
||||
run_id:
|
||||
description: "Run ID"
|
||||
value: ${{ steps.pr.outputs.run_id }}
|
||||
gradio_version:
|
||||
description: "Gradio version"
|
||||
value: ${{ steps.pr.outputs.gradio_version }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: gradio-app/github/actions/filter-paths@main
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ inputs.token }}
|
||||
filter: ${{ inputs.filter }}
|
||||
- name: get gradio version
|
||||
id: gradio_version
|
||||
shell: bash
|
||||
run: |
|
||||
GRADIO_VERSION=$(curl -s https://pypi.org/pypi/gradio/json | grep -o '"version":"[^"]*"' | cut -d'"' -f4 | head -n 1)
|
||||
echo "gradio_version=$GRADIO_VERSION" >> $GITHUB_OUTPUT
|
||||
- name: convert to JSON
|
||||
uses: gradio-app/github/actions/input-to-json@main
|
||||
with:
|
||||
path: output.json
|
||||
sha: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
source_repo: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
||||
source_branch: ${{ github.event.pull_request.head.ref || github.ref }}
|
||||
pr_number: ${{ github.event.pull_request.number || 'false'}}
|
||||
should_run: ${{ steps.changes.outputs.match }}
|
||||
labels: "[${{ join(github.event.pull_request.labels.*.name, ', ') }}]"
|
||||
run_id: ${{ github.run_id }}
|
||||
gradio_version: ${{ steps.gradio_version.outputs.gradio_version }}
|
||||
- name: cat json
|
||||
run: cat output.json
|
||||
shell: bash
|
||||
- name: upload JSON
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: changes
|
||||
path: output.json
|
||||
- name: set outputs
|
||||
id: pr
|
||||
uses: gradio-app/github/actions/json-to-output@main
|
||||
with:
|
||||
path: output.json
|
||||
- name: echo outputs
|
||||
run: echo "${{ toJson(steps.pr.outputs) }}"
|
||||
shell: bash
|
||||
@@ -0,0 +1,140 @@
|
||||
name: "install all deps"
|
||||
description: "Install all deps"
|
||||
|
||||
inputs:
|
||||
skip_build:
|
||||
description: "Skip build"
|
||||
default: "false"
|
||||
skip_gradio_install:
|
||||
description: "Skip installing gradio/client and building frontend (use when installing from pre-built wheels)"
|
||||
default: "false"
|
||||
skip_docs_gen:
|
||||
description: "Skip generating website docs (CSS-var docs + website JSON). Not needed for functional/browser tests; saves ~80s."
|
||||
default: "false"
|
||||
test:
|
||||
description: "Test"
|
||||
default: "false"
|
||||
python_version:
|
||||
description: "Python version"
|
||||
default: "3.10"
|
||||
os:
|
||||
description: "OS"
|
||||
default: "ubuntu-latest"
|
||||
outputs:
|
||||
venv_activate:
|
||||
description: "Venv activate"
|
||||
value: ${{ steps.venv.outputs.venv_activate }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Set venv binary path (linux)
|
||||
if: ${{ inputs.os == 'ubuntu-latest' }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "VENV_ACTIVATE=venv/bin/activate" >> $GITHUB_ENV
|
||||
- name: Set venv binary path (windows)
|
||||
if: ${{ inputs.os == 'windows-latest' }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "VENV_ACTIVATE=venv/Scripts/activate" >> $GITHUB_ENV
|
||||
- name: log venv binary path
|
||||
id: venv
|
||||
shell: bash
|
||||
run: |
|
||||
echo "venv_activate=$VENV_ACTIVATE" >> $GITHUB_OUTPUT
|
||||
- name: Install Python
|
||||
id: setup-python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ inputs.python_version }}
|
||||
cache-dependency-path: |
|
||||
client/python/requirements.txt
|
||||
requirements.txt
|
||||
requirements-mcp.txt
|
||||
test/requirements.txt
|
||||
- name: Set uv exclude-newer
|
||||
shell: bash
|
||||
run: |
|
||||
echo "UV_EXCLUDE_NEWER=7 days" >> "$GITHUB_ENV"
|
||||
- name: Install uv (linux)
|
||||
if: ${{ inputs.os == 'ubuntu-latest' }}
|
||||
shell: bash
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/0.11.3/install.sh | sh
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
- name: Install uv (windows)
|
||||
if: ${{ inputs.os == 'windows-latest' }}
|
||||
shell: bash
|
||||
run: |
|
||||
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.11.3/install.ps1 | iex"
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
- uses: actions/cache@v4
|
||||
id: cache
|
||||
with:
|
||||
path: |
|
||||
venv/**
|
||||
client/python/venv
|
||||
restore-keys: |
|
||||
gradio-lib-main-${{inputs.python_version}}-${{ steps.setup-python.outputs.python-version }}-${{inputs.os}}-latest-pip-
|
||||
key: "gradio-lib-main-${{inputs.python_version}}-${{ steps.setup-python.outputs.python-version }}-${{inputs.os}}-latest-pip-${{ hashFiles('client/python/requirements.txt') }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('test/requirements.txt') }}-${{ hashFiles('client/python/test/requirements.txt') }}${{ inputs.test == 'true' && '-test' || ''}}"
|
||||
- name: Create env
|
||||
shell: bash
|
||||
run: |
|
||||
uv venv --allow-existing venv --python=${{ inputs.python_version }}
|
||||
- name: Cache ffmpeg
|
||||
id: cache-ffmpeg
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ffmpeg-bin
|
||||
key: ffmpeg-${{ inputs.os }}-7.0.2-v1
|
||||
- name: Install ffmpeg (linux)
|
||||
if: ${{ inputs.os == 'ubuntu-latest' && steps.cache-ffmpeg.outputs.cache-hit != 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p ffmpeg-bin
|
||||
curl -fsSL https://johnvansickle.com/ffmpeg/releases/ffmpeg-7.0.2-amd64-static.tar.xz -o /tmp/ffmpeg.tar.xz
|
||||
tar -xf /tmp/ffmpeg.tar.xz -C /tmp
|
||||
cp /tmp/ffmpeg-7.0.2-amd64-static/ffmpeg /tmp/ffmpeg-7.0.2-amd64-static/ffprobe ffmpeg-bin/
|
||||
- name: Install ffmpeg (windows)
|
||||
if: ${{ inputs.os == 'windows-latest' && steps.cache-ffmpeg.outputs.cache-hit != 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p ffmpeg-bin
|
||||
curl -fsSL https://github.com/GyanD/codexffmpeg/releases/download/7.0.2/ffmpeg-7.0.2-essentials_build.zip -o ffmpeg.zip
|
||||
unzip -q ffmpeg.zip
|
||||
mv ffmpeg-7.0.2-essentials_build/bin/ffmpeg.exe ffmpeg-7.0.2-essentials_build/bin/ffprobe.exe ffmpeg-bin/
|
||||
- name: Add ffmpeg to PATH
|
||||
shell: bash
|
||||
run: echo "$GITHUB_WORKSPACE/ffmpeg-bin" >> "$GITHUB_PATH"
|
||||
- name: Install test dependencies
|
||||
if: inputs.test == 'true' && steps.cache.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
. ${{ env.VENV_ACTIVATE }}
|
||||
uv pip install -r test/requirements.txt --torch-backend cpu
|
||||
uv pip install -r client/python/test/requirements.txt
|
||||
- name: Install Gradio and Client Libraries Locally
|
||||
if: inputs.skip_gradio_install != 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
. ${{ env.VENV_ACTIVATE }}
|
||||
uv pip install -e client/python
|
||||
uv pip install -e ".[oauth, mcp]"
|
||||
- name: install-frontend
|
||||
if: inputs.skip_gradio_install != 'true'
|
||||
uses: "gradio-app/gradio/.github/actions/install-frontend-deps@main"
|
||||
with:
|
||||
skip_build: ${{ inputs.skip_build }}
|
||||
- name: generate css vars docs
|
||||
shell: bash
|
||||
if: inputs.skip_gradio_install != 'true' && inputs.skip_docs_gen != 'true' && inputs.os == 'ubuntu-latest'
|
||||
run: |
|
||||
. ${{ env.VENV_ACTIVATE }}
|
||||
[ -f scripts/generate_css_vars_docs.py ] && python scripts/generate_css_vars_docs.py || true
|
||||
- name: generate json
|
||||
shell: bash
|
||||
if: inputs.skip_gradio_install != 'true' && inputs.skip_docs_gen != 'true' && inputs.os == 'ubuntu-latest'
|
||||
run: |
|
||||
. ${{ env.VENV_ACTIVATE }}
|
||||
uv pip install boto3 markdown requests && python js/_website/generate_jsons/generate.py
|
||||
@@ -0,0 +1,46 @@
|
||||
name: "install frontend"
|
||||
description: "Install frontend deps"
|
||||
|
||||
inputs:
|
||||
skip_build:
|
||||
description: "Skip build"
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: actions/cache@v4
|
||||
id: frontend-core-cache
|
||||
if: inputs.skip_build == 'false'
|
||||
with:
|
||||
path: |
|
||||
gradio/templates/**
|
||||
js/preview/dist/**
|
||||
key: gradio-lib-main-front-end-v2-${{ hashFiles('js/**', 'client/js/**', 'pnpm-lock.yaml', 'scripts/download_offline_assets.py')}}
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # @v4
|
||||
with:
|
||||
version: 10.17.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
- name: Install deps
|
||||
shell: bash
|
||||
run: pnpm i --frozen-lockfile --ignore-scripts
|
||||
- name: Build Css
|
||||
shell: bash
|
||||
run: pnpm css
|
||||
- name: Build frontend
|
||||
if: inputs.skip_build == 'false' && steps.frontend-core-cache.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: pnpm build
|
||||
- name: Download offline assets
|
||||
# Bundles the self-hosted fonts/Bokeh/FFmpeg/iframe-resizer into
|
||||
# gradio/templates/frontend/static so Gradio works offline. Must run after
|
||||
# `pnpm build` (which empties the output dir) and before the wheel is built.
|
||||
# Skipped on a cache hit because the cached templates already include them.
|
||||
if: inputs.skip_build == 'false' && steps.frontend-core-cache.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: python3 scripts/download_offline_assets.py
|
||||
@@ -0,0 +1,157 @@
|
||||
rules:
|
||||
- id: third-party-action-not-pinned-to-commit-sha
|
||||
patterns:
|
||||
- pattern-inside: "{steps: ...}"
|
||||
- pattern: |
|
||||
uses: "$USES"
|
||||
- metavariable-pattern:
|
||||
metavariable: $USES
|
||||
language: generic
|
||||
patterns:
|
||||
- pattern-not-regex: ^[.]/
|
||||
- pattern-not-regex: ^actions/
|
||||
- pattern-not-regex: ^github/
|
||||
- pattern-not-regex: ^gradio-app/gradio
|
||||
- pattern-not-regex: ^gradio-app/github
|
||||
- pattern-not-regex: "@[0-9a-f]{40}$"
|
||||
- pattern-not-regex: ^docker://.*@sha256:[0-9a-f]{64}$
|
||||
- pattern-not-regex: ^docker://docker$
|
||||
message:
|
||||
An action sourced from a third-party repository on GitHub is not pinned
|
||||
to a full length commit SHA. Pinning an action to a full length commit SHA
|
||||
is currently the only way to use an action as an immutable release.
|
||||
Pinning to a particular SHA helps mitigate the risk of a bad actor adding
|
||||
a backdoor to the action's repository, as they would need to generate a
|
||||
SHA-1 collision for a valid Git object payload.
|
||||
languages:
|
||||
- yaml
|
||||
severity: WARNING
|
||||
metadata:
|
||||
cwe:
|
||||
- "CWE-1357: Reliance on Insufficiently Trustworthy Component"
|
||||
- "CWE-353: Missing Support for Integrity Check"
|
||||
owasp: A06:2021 - Vulnerable and Outdated Components
|
||||
references:
|
||||
- https://owasp.org/Top10/A06_2021-Vulnerable_and_Outdated_Components
|
||||
- https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions
|
||||
category: security
|
||||
technology:
|
||||
- github-actions
|
||||
subcategory:
|
||||
- vuln
|
||||
likelihood: LOW
|
||||
impact: LOW
|
||||
confidence: HIGH
|
||||
license: Commons Clause License Condition v1.0[LGPL-2.1-only]
|
||||
vulnerability_class:
|
||||
- Cryptographic Issues
|
||||
- Other
|
||||
|
||||
- id: no-bare-httpx-url-fetch-ssrf
|
||||
languages:
|
||||
- python
|
||||
severity: ERROR
|
||||
message: >
|
||||
Do not use bare `httpx` request functions to fetch URLs in components or
|
||||
media/processing utilities. These code paths handle user-influenced URLs
|
||||
(e.g. an `Image`/`Gallery` SVG source or an `Audio` path) and a plain
|
||||
`httpx` request has no SSRF protection (no private-IP filter, domain
|
||||
allow-list, or redirect re-validation), which can leak internal responses
|
||||
to the client (CWE-918). Route the request through `safehttpx` instead,
|
||||
e.g. `processing_utils.async_ssrf_protected_get` /
|
||||
`async_ssrf_protected_download`, which use `PUBLIC_HOSTNAME_WHITELIST`.
|
||||
paths:
|
||||
include:
|
||||
- "**/gradio/components/"
|
||||
- "**/gradio/image_utils.py"
|
||||
- "**/gradio/processing_utils.py"
|
||||
patterns:
|
||||
- pattern-either:
|
||||
- pattern: httpx.get(...)
|
||||
- pattern: httpx.post(...)
|
||||
- pattern: httpx.put(...)
|
||||
- pattern: httpx.patch(...)
|
||||
- pattern: httpx.delete(...)
|
||||
- pattern: httpx.head(...)
|
||||
- pattern: httpx.request(...)
|
||||
- pattern: httpx.stream(...)
|
||||
metadata:
|
||||
category: security
|
||||
owasp:
|
||||
- A10:2021 - Server-Side Request Forgery (SSRF)
|
||||
cwe:
|
||||
- "CWE-918: Server-Side Request Forgery (SSRF)"
|
||||
technology:
|
||||
- python
|
||||
references:
|
||||
- https://github.com/gradio-app/gradio/security/advisories/GHSA-3xvj-7669-6whx
|
||||
subcategory:
|
||||
- vuln
|
||||
likelihood: MEDIUM
|
||||
impact: HIGH
|
||||
confidence: HIGH
|
||||
license: Commons Clause License Condition v1.0[LGPL-2.1-only]
|
||||
vulnerability_class:
|
||||
- Server-Side Request Forgery (SSRF)
|
||||
|
||||
- id: insecure-file-permissions
|
||||
languages:
|
||||
- python
|
||||
severity: ERROR
|
||||
message: These permissions `$BITS` are widely permissive and grant access to
|
||||
more people than may be necessary. A good default is `0o644` which gives
|
||||
read and write access to yourself and read access to everyone else.
|
||||
patterns:
|
||||
- pattern-inside: os.$METHOD(...)
|
||||
- pattern-either:
|
||||
- patterns:
|
||||
- pattern: os.$METHOD($FILE, $BITS, ...)
|
||||
- metavariable-comparison:
|
||||
comparison: $BITS >= 0o650 and $BITS < 0o100000
|
||||
- patterns:
|
||||
- pattern: os.$METHOD($FILE, $BITS)
|
||||
- metavariable-comparison:
|
||||
comparison: $BITS >= 0o100650
|
||||
- patterns:
|
||||
- pattern: os.$METHOD($FILE, $BITS, ...)
|
||||
- metavariable-pattern:
|
||||
metavariable: $BITS
|
||||
patterns:
|
||||
- pattern-either:
|
||||
- pattern: <... stat.S_IWGRP ...>
|
||||
- pattern: <... stat.S_IXGRP ...>
|
||||
- pattern: <... stat.S_IWOTH ...>
|
||||
- pattern: <... stat.S_IXOTH ...>
|
||||
- pattern: <... stat.S_IRWXO ...>
|
||||
- pattern: <... stat.S_IRWXG ...>
|
||||
- patterns:
|
||||
- pattern: os.$METHOD($FILE, $EXPR | $MOD, ...)
|
||||
- metavariable-comparison:
|
||||
comparison: $MOD == 0o111
|
||||
- metavariable-pattern:
|
||||
metavariable: $METHOD
|
||||
patterns:
|
||||
- pattern-either:
|
||||
- pattern: chmod
|
||||
- pattern: lchmod
|
||||
- pattern: fchmod
|
||||
metadata:
|
||||
category: security
|
||||
owasp:
|
||||
- A01:2021 - Broken Access Control
|
||||
cwe:
|
||||
- "CWE-276: Incorrect Default Permissions"
|
||||
technology:
|
||||
- python
|
||||
references:
|
||||
- https://owasp.org/Top10/A01_2021-Broken_Access_Control
|
||||
cwe2022-top25: true
|
||||
cwe2021-top25: true
|
||||
subcategory:
|
||||
- vuln
|
||||
likelihood: LOW
|
||||
impact: MEDIUM
|
||||
confidence: MEDIUM
|
||||
license: Commons Clause License Condition v1.0[LGPL-2.1-only]
|
||||
vulnerability_class:
|
||||
- Improper Authorization
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"gradio": [
|
||||
"client/python/**",
|
||||
"gradio/**",
|
||||
"requirements.txt",
|
||||
".github/**",
|
||||
"scripts/**",
|
||||
"test/**"
|
||||
],
|
||||
"js": [
|
||||
"js/**",
|
||||
"client/js/**",
|
||||
".github/**",
|
||||
"package.json",
|
||||
"pnpm-lock.yaml",
|
||||
"tsconfig.json",
|
||||
".config/**"
|
||||
],
|
||||
"functional": [
|
||||
".github/**",
|
||||
"client/**",
|
||||
"demo/**",
|
||||
"gradio/**",
|
||||
"js/**",
|
||||
"scripts/**",
|
||||
|
||||
"globals.d.ts",
|
||||
"package.json",
|
||||
"pnpm-lock.yaml",
|
||||
"pyproject.toml",
|
||||
"requirements.txt",
|
||||
".config/**"
|
||||
],
|
||||
"visual": [
|
||||
".github/workflows/deploy-chromatic.yml",
|
||||
"js!(/_website)/**",
|
||||
"package.json"
|
||||
],
|
||||
"website": [
|
||||
"js/_website/**",
|
||||
"package.json",
|
||||
"pnpm-lock.yaml",
|
||||
"guides/**",
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"gradio/**",
|
||||
"client/**",
|
||||
"demo/**",
|
||||
".github/deploy-website.yml"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 30
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: wontfix
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
@@ -0,0 +1,37 @@
|
||||
name: Comment on pull request without race conditions
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
pr_number:
|
||||
type: string
|
||||
message:
|
||||
required: true
|
||||
type: string
|
||||
tag:
|
||||
required: false
|
||||
type: string
|
||||
default: "previews"
|
||||
additional_text:
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
secrets:
|
||||
gh_token:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
environment: comment_pr
|
||||
concurrency:
|
||||
group: ${{inputs.pr_number || inputs.tag}}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: comment on pr
|
||||
uses: "gradio-app/github/actions/comment-pr@main"
|
||||
with:
|
||||
gh_token: ${{ secrets.gh_token }}
|
||||
tag: ${{ inputs.tag }}
|
||||
pr_number: ${{ inputs.pr_number}}
|
||||
message: ${{ inputs.message }}
|
||||
additional_text: ${{ inputs.additional_text }}
|
||||
@@ -0,0 +1,42 @@
|
||||
# safe runs from main
|
||||
|
||||
name: Delete Stale Spaces
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
daysStale:
|
||||
description: "How stale a space needs to be to be deleted (days)"
|
||||
required: true
|
||||
default: "7"
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
delete-old-spaces:
|
||||
permissions:
|
||||
contents: read
|
||||
environment: deploy_spaces
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install pip
|
||||
run: python -m pip install pip wheel requests
|
||||
- name: Install Hub Client Library
|
||||
run: pip install huggingface-hub==0.9.1
|
||||
- name: Set daysStale
|
||||
env:
|
||||
DEFAULT_DAYS_STALE: "7"
|
||||
DAYS_STALE: ${{ github.event.inputs.daysStale || env.DEFAULT_DAYS_STALE}}
|
||||
run: echo DAYS_STALE= "$DAYS_STALE" >> $GITHUB_ENV
|
||||
- name: Find and delete stale spaces
|
||||
run: |
|
||||
python scripts/delete_old_spaces.py $DAYS_STALE \
|
||||
gradio-pr-deploys \
|
||||
${{ secrets.WEBSITE_SPACES_DEPLOY_TOKEN }}
|
||||
@@ -0,0 +1,172 @@
|
||||
name: "frontend-profiling"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
concurrency:
|
||||
group: "frontend-profiling-${{ github.event.pull_request.number }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
benchmark:
|
||||
env:
|
||||
UV_EXCLUDE_NEWER: 7 days
|
||||
PLAYWRIGHT_VERSION: 1.60.0
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
name: "frontend-benchmark"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: "gradio-app/gradio/.github/actions/changes@main"
|
||||
id: changes
|
||||
with:
|
||||
filter: "js"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Find latest release tag
|
||||
if: steps.changes.outputs.should_run == 'true'
|
||||
id: latest_tag
|
||||
run: echo "tag=$(git tag --list 'gradio@*' --sort=-v:refname | head -n 1)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup Python
|
||||
if: steps.changes.outputs.should_run == 'true'
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install uv
|
||||
if: steps.changes.outputs.should_run == 'true'
|
||||
run: curl -LsSf https://astral.sh/uv/0.11.3/install.sh | sh
|
||||
|
||||
- name: Install pnpm
|
||||
if: steps.changes.outputs.should_run == 'true'
|
||||
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # @v4
|
||||
with:
|
||||
version: 10.17.0
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.changes.outputs.should_run == 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
|
||||
# Install the benchmark "base" (latest release) straight from PyPI instead
|
||||
# of checking out the tag and rebuilding it — the published wheel already
|
||||
# ships the built frontend (gradio/templates), so this drops the ~4min base
|
||||
# `pnpm build` on every run. We stay on the PR checkout the whole job: the
|
||||
# benchmark spec + demo are the PR's versions for both runs, and the demo is
|
||||
# launched as `python <runner>.py` (the runner/demo dir is on sys.path, not
|
||||
# the cwd), so `import gradio` resolves to the installed package — the wheel
|
||||
# here, the editable PR build later. Base and PR use separate venvs so the
|
||||
# PR's editable install can't shadow the base wheel.
|
||||
- name: Install base gradio from the published release (base)
|
||||
if: steps.changes.outputs.should_run == 'true'
|
||||
run: |
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
uv venv --python=3.10 venv_base
|
||||
. venv_base/bin/activate
|
||||
TAG="${{ steps.latest_tag.outputs.tag }}"
|
||||
uv pip install "gradio==${TAG#gradio@}"
|
||||
|
||||
- name: Install frontend test deps (base)
|
||||
if: steps.changes.outputs.should_run == 'true'
|
||||
# node_modules is only needed to run the Playwright benchmark (no build).
|
||||
run: |
|
||||
pnpm i --frozen-lockfile --ignore-scripts
|
||||
pnpm add -Dw --ignore-scripts playwright@$PLAYWRIGHT_VERSION @playwright/test@$PLAYWRIGHT_VERSION
|
||||
|
||||
- name: Cache Playwright Browsers
|
||||
if: steps.changes.outputs.should_run == 'true'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
playwright-${{ runner.os }}-
|
||||
|
||||
- name: Install Playwright (base)
|
||||
if: steps.changes.outputs.should_run == 'true'
|
||||
timeout-minutes: 3
|
||||
run: pnpm exec playwright install chromium --only-shell
|
||||
|
||||
- name: Run benchmark (base)
|
||||
if: steps.changes.outputs.should_run == 'true'
|
||||
run: |
|
||||
. venv_base/bin/activate
|
||||
PERF_RESULTS_FILE=/tmp/bench_base.json pnpm exec playwright test \
|
||||
--config .config/playwright.config.js \
|
||||
js/spa/test/big_complex_demo.spec.ts
|
||||
|
||||
- name: Install and build PR
|
||||
if: steps.changes.outputs.should_run == 'true'
|
||||
run: |
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
uv venv --allow-existing venv --python=3.10
|
||||
. venv/bin/activate
|
||||
uv pip install -e client/python
|
||||
uv pip install -e ".[oauth,mcp]"
|
||||
pnpm install --no-frozen-lockfile
|
||||
pnpm css && pnpm build
|
||||
|
||||
- name: Install Playwright (PR)
|
||||
if: steps.changes.outputs.should_run == 'true'
|
||||
timeout-minutes: 3
|
||||
run: pnpm exec playwright install chromium --only-shell
|
||||
|
||||
- name: Run benchmark (PR)
|
||||
if: steps.changes.outputs.should_run == 'true'
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
PERF_RESULTS_FILE=/tmp/bench_pr.json pnpm exec playwright test \
|
||||
--config .config/playwright.config.js \
|
||||
js/spa/test/big_complex_demo.spec.ts
|
||||
|
||||
- name: Compare results
|
||||
id: compare
|
||||
if: steps.changes.outputs.should_run == 'true'
|
||||
env:
|
||||
BASE_TAG: ${{ steps.latest_tag.outputs.tag }}
|
||||
run: node scripts/compare_frontend_benchmarks.js
|
||||
|
||||
- name: Post or update PR comment
|
||||
if: steps.compare.outputs.failed == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const body = fs.readFileSync('/tmp/bench_comment.md', 'utf8');
|
||||
const marker = '<!-- frontend-perf-benchmark -->';
|
||||
const commentBody = marker + '\n' + body;
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
|
||||
const existing = comments.find(c => c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body: commentBody,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: commentBody,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
name: Generate changeset
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["trigger-changeset"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
env:
|
||||
CI: true
|
||||
|
||||
concurrency:
|
||||
group: "${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}"
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
get-pr:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
outputs:
|
||||
found_pr: ${{ steps.pr_details.outputs.found_pr }}
|
||||
pr_number: ${{ steps.pr_details.outputs.pr_number }}
|
||||
source_repo: ${{ steps.pr_details.outputs.source_repo }}
|
||||
source_branch: ${{ steps.pr_details.outputs.source_branch }}
|
||||
actor: ${{ steps.pr_details.outputs.actor }}
|
||||
sha: ${{ steps.pr_details.outputs.sha }}
|
||||
steps:
|
||||
- name: get pr details
|
||||
id: pr_details
|
||||
uses: gradio-app/github/actions/find-pr@main
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
comment-changes-start:
|
||||
uses: "./.github/workflows/comment-queue.yml"
|
||||
needs: get-pr
|
||||
secrets:
|
||||
gh_token: ${{ secrets.COMMENT_TOKEN }}
|
||||
with:
|
||||
pr_number: ${{ needs.get-pr.outputs.pr_number }}
|
||||
message: changes~pending~null
|
||||
version:
|
||||
permissions:
|
||||
contents: read
|
||||
environment: changeset
|
||||
name: version
|
||||
needs: get-pr
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.get-pr.outputs.found_pr == 'true'
|
||||
outputs:
|
||||
skipped: ${{ steps.version.outputs.skipped }}
|
||||
comment_url: ${{ steps.version.outputs.comment_url }}
|
||||
approved: ${{ steps.version.outputs.approved }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ needs.get-pr.outputs.source_repo }}
|
||||
ref: ${{ needs.get-pr.outputs.source_branch }}
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.COMMENT_TOKEN }}
|
||||
- name: generate changeset
|
||||
id: version
|
||||
uses: "gradio-app/github/actions/generate-changeset@main"
|
||||
with:
|
||||
github_token: ${{ secrets.CHANGESET_GITHUB_TOKEN }}
|
||||
main_pkg: gradio
|
||||
pr_number: ${{ needs.get-pr.outputs.pr_number }}
|
||||
branch_name: ${{ needs.get-pr.outputs.source_branch }}
|
||||
actor: ${{ needs.get-pr.outputs.actor }}
|
||||
update-status:
|
||||
permissions:
|
||||
actions: read
|
||||
statuses: write
|
||||
runs-on: ubuntu-latest
|
||||
needs: [version, get-pr]
|
||||
steps:
|
||||
- name: update status
|
||||
uses: gradio-app/github/actions/commit-status@main
|
||||
with:
|
||||
sha: ${{ needs.get-pr.outputs.sha }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: "Changeset Results"
|
||||
pr: ${{ needs.get-pr.outputs.pr_number }}
|
||||
# Temporarily disabled: the changeset approval requirement was more hassle than
|
||||
# it was worth (the check failed whenever a maintainer hadn't ticked the approval
|
||||
# checkbox). Force the check to pass for now. To re-enable, restore the line below.
|
||||
# result: ${{ needs.version.outputs.approved == 'true' && 'success' || 'failure' }}
|
||||
result: success
|
||||
type: all
|
||||
comment-changes-skipped:
|
||||
uses: "./.github/workflows/comment-queue.yml"
|
||||
needs: [get-pr, version]
|
||||
if: needs.version.result == 'success' && needs.version.outputs.skipped == 'true'
|
||||
secrets:
|
||||
gh_token: ${{ secrets.COMMENT_TOKEN }}
|
||||
with:
|
||||
pr_number: ${{ needs.get-pr.outputs.pr_number }}
|
||||
message: changes~warning~https://github.com/gradio-app/gradio/actions/runs/${{github.run_id}}/
|
||||
comment-changes-success:
|
||||
uses: "./.github/workflows/comment-queue.yml"
|
||||
needs: [get-pr, version]
|
||||
if: needs.version.result == 'success' && needs.version.outputs.skipped == 'false'
|
||||
secrets:
|
||||
gh_token: ${{ secrets.COMMENT_TOKEN }}
|
||||
with:
|
||||
pr_number: ${{ needs.get-pr.outputs.pr_number }}
|
||||
message: changes~success~${{ needs.version.outputs.comment_url }}
|
||||
comment-changes-failure:
|
||||
uses: "./.github/workflows/comment-queue.yml"
|
||||
needs: [get-pr, version]
|
||||
if: always() && needs.version.result == 'failure'
|
||||
secrets:
|
||||
gh_token: ${{ secrets.COMMENT_TOKEN }}
|
||||
with:
|
||||
pr_number: ${{ needs.get-pr.outputs.pr_number }}
|
||||
message: changes~failure~https://github.com/gradio-app/gradio/actions/runs/${{github.run_id}}/
|
||||
@@ -0,0 +1,58 @@
|
||||
name: "npm"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
CI: true
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
|
||||
|
||||
concurrency:
|
||||
group: "${{ github.event.pull_request.number }}-${{ github.ref_name }}-${{ github.workflow }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
name: "changes"
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_run: ${{ steps.changes.outputs.should_run }}
|
||||
sha: ${{ steps.changes.outputs.sha }}
|
||||
pr_number: ${{ steps.changes.outputs.pr_number }}
|
||||
source_branch: ${{ steps.changes.outputs.source_branch }}
|
||||
source_repo: ${{ steps.changes.outputs.source_repo }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: "gradio-app/gradio/.github/actions/changes@main"
|
||||
id: changes
|
||||
with:
|
||||
filter: "js"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
preview:
|
||||
permissions:
|
||||
contents: read
|
||||
name: npm-previews
|
||||
runs-on: ubuntu-22.04
|
||||
needs: changes
|
||||
if: needs.changes.outputs.should_run == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: install dependencies
|
||||
uses: "gradio-app/gradio/.github/actions/install-frontend-deps@main"
|
||||
with:
|
||||
skip_build: true
|
||||
- name: Setup Node.js 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
- name: package
|
||||
run: pnpm package
|
||||
- name: build client
|
||||
run: pnpm --filter @gradio/client --filter @gradio/preview build
|
||||
- name: publish npm previews
|
||||
run: pnpx pkg-pr-new@latest publish './js/*' './client/js' --comment=off
|
||||
@@ -0,0 +1,77 @@
|
||||
name: "previews-build"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
CI: true
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
|
||||
|
||||
concurrency:
|
||||
group: "${{ github.event.pull_request.number }}-${{ github.ref_name }}-${{ github.workflow }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: "changes"
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
outputs:
|
||||
should_run: ${{ steps.changes.outputs.should_run }}
|
||||
sha: ${{ steps.changes.outputs.sha }}
|
||||
gradio_version: ${{ steps.changes.outputs.gradio_version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: "gradio-app/gradio/.github/actions/changes@main"
|
||||
id: changes
|
||||
with:
|
||||
filter: "functional"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
build:
|
||||
permissions:
|
||||
contents: read
|
||||
name: "previews-build"
|
||||
runs-on: ubuntu-22.04
|
||||
needs: changes
|
||||
if: needs.changes.outputs.should_run == 'true' || github.event_name == 'workflow_dispatch'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: install dependencies
|
||||
uses: "gradio-app/gradio/.github/actions/install-all-deps@main"
|
||||
with:
|
||||
python_version: "3.12"
|
||||
skip_docs_gen: true
|
||||
- name: install deps
|
||||
run: python -m pip install build
|
||||
- name: Build pr package
|
||||
run: |
|
||||
python -c 'import json; j = json.load(open("gradio/package.json")); j["version"] = "${{ needs.changes.outputs.gradio_version }}"; json.dump(j, open("gradio/package.json", "w"))'
|
||||
python3 -m build -w
|
||||
- name: Upload Python package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gradio-build
|
||||
path: dist/gradio-${{ needs.changes.outputs.gradio_version }}-py3-none-any.whl
|
||||
- name: copy demos
|
||||
uses: "gradio-app/github/actions/copy-demos@main"
|
||||
with:
|
||||
gradio_version: "https://huggingface.co/buckets/gradio/pypi-previews/resolve/${{ needs.changes.outputs.sha }}/gradio-${{ needs.changes.outputs.gradio_version }}-py3-none-any.whl"
|
||||
gradio_client_version: "gradio-client @ git+https://github.com/gradio-app/gradio@${{ needs.changes.outputs.sha }}#subdirectory=client/python"
|
||||
config_path: ".config/demos.json"
|
||||
- name: upload demos
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: all_demos
|
||||
path: demo
|
||||
- name: Build JS client
|
||||
run: pnpm --filter @gradio/client build
|
||||
- name: Upload JS client dist artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: js-client-dist
|
||||
path: client/js/dist
|
||||
@@ -0,0 +1,156 @@
|
||||
name: "previews-deploy"
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["previews-build"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
concurrency:
|
||||
group: "${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}-${{ github.workflow_ref }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: "changes"
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
permissions:
|
||||
actions: read
|
||||
outputs:
|
||||
should_run: ${{ steps.json.outputs.should_run }}
|
||||
sha: ${{ steps.json.outputs.sha }}
|
||||
pr_number: ${{ steps.json.outputs.pr_number }}
|
||||
source_branch: ${{ steps.json.outputs.source_branch }}
|
||||
source_repo: ${{ steps.json.outputs.source_repo }}
|
||||
labels: ${{ steps.json.outputs.labels }}
|
||||
run_id: ${{ steps.json.outputs.run_id }}
|
||||
gradio_version: ${{ steps.json.outputs.gradio_version }}
|
||||
steps:
|
||||
- name: Download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: changes
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
- uses: gradio-app/github/actions/json-to-output@main
|
||||
id: json
|
||||
with:
|
||||
path: output.json
|
||||
|
||||
comment-spaces-start:
|
||||
needs: changes
|
||||
uses: "./.github/workflows/comment-queue.yml"
|
||||
if: ${{ needs.changes.outputs.should_run == 'true' }}
|
||||
secrets:
|
||||
gh_token: ${{ secrets.COMMENT_TOKEN }}
|
||||
with:
|
||||
pr_number: ${{ needs.changes.outputs.pr_number }}
|
||||
message: spaces~pending~null
|
||||
deploy:
|
||||
name: "previews-deploy"
|
||||
environment: deploy_spaces
|
||||
outputs:
|
||||
space_url: ${{ steps.upload-demo.outputs.SPACE_URL }}
|
||||
needs: changes
|
||||
if: ${{ (github.event.workflow_run.conclusion == 'success' && needs.changes.outputs.should_run == 'true') || github.event.workflow_run.event == 'workflow_dispatch' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: list artifacts
|
||||
run: ls -R .
|
||||
|
||||
- name: Set wheel name
|
||||
id: set_wheel_name
|
||||
run: |
|
||||
wheel_file=$(find ./gradio-build -maxdepth 1 -type f -name "*.whl" -print -quit)
|
||||
echo "wheel_name=$wheel_file" >> $GITHUB_OUTPUT
|
||||
- name: Install hf CLI
|
||||
run: curl -LsSf https://hf.co/cli/install.sh | bash -s
|
||||
- name: Upload wheel
|
||||
run: hf buckets cp ${{ steps.set_wheel_name.outputs.wheel_name }} hf://buckets/gradio/pypi-previews/${{ needs.changes.outputs.sha }}/
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.PYPI_PREVIEWS_TOKEN }}
|
||||
|
||||
- name: Upload JS client dist to HF
|
||||
run: |
|
||||
if [ -d ./js-client-dist ]; then
|
||||
hf buckets sync ./js-client-dist hf://buckets/gradio/npm-previews/${{ needs.changes.outputs.sha }}
|
||||
else
|
||||
echo "Skipping JS client dist upload: ./js-client-dist not found."
|
||||
fi
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.PYPI_PREVIEWS_TOKEN }}
|
||||
|
||||
- name: Install Hub Client Library
|
||||
run: |
|
||||
python3 -m venv venv
|
||||
. venv/bin/activate
|
||||
pip install huggingface-hub==0.23.2
|
||||
# temporary, but ensures the script cannot be modified in a PR
|
||||
- name: Get deploy scripts
|
||||
run: |
|
||||
mkdir scripts
|
||||
curl https://raw.githubusercontent.com/gradio-app/gradio/main/scripts/upload_demo_to_space.py -o scripts/upload_demo_to_space.py
|
||||
curl https://raw.githubusercontent.com/gradio-app/gradio/main/scripts/upload_website_demos.py -o scripts/upload_website_demos.py
|
||||
- name: make dirs
|
||||
run: mkdir -p demo && mv all_demos/* demo/
|
||||
- name: Upload demo to spaces
|
||||
if: github.event.workflow_run.event == 'pull_request'
|
||||
id: upload-demo
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python scripts/upload_demo_to_space.py all_demos \
|
||||
gradio-pr-deploys/pr-${{ needs.changes.outputs.pr_number }}-all-demos \
|
||||
${{ secrets.WEBSITE_SPACES_DEPLOY_TOKEN }} \
|
||||
--gradio-version ${{ needs.changes.outputs.gradio_version }} > url.txt
|
||||
echo "SPACE_URL=$(tail -n 1 url.txt)" >> $GITHUB_OUTPUT
|
||||
- name: Upload Website Demos
|
||||
if: github.event.workflow_run.event == 'workflow_dispatch'
|
||||
id: upload-website-demos
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python scripts/upload_website_demos.py --AUTH_TOKEN ${{ secrets.WEBSITE_SPACES_DEPLOY_TOKEN }} \
|
||||
--WHEEL_URL https://huggingface.co/buckets/gradio/pypi-previews/resolve/${{ needs.changes.outputs.sha }}/ \
|
||||
--CLIENT_URL "gradio-client @ git+https://github.com/gradio-app/gradio@${{ needs.changes.outputs.sha }}#subdirectory=client/python" \
|
||||
--GRADIO_VERSION ${{ needs.changes.outputs.gradio_version }}
|
||||
|
||||
comment-spaces-success:
|
||||
uses: "./.github/workflows/comment-queue.yml"
|
||||
needs: [deploy, changes]
|
||||
if: needs.changes.outputs.should_run == 'true' && needs.deploy.result == 'success'
|
||||
secrets:
|
||||
gh_token: ${{ secrets.COMMENT_TOKEN }}
|
||||
with:
|
||||
pr_number: ${{ needs.changes.outputs.pr_number }}
|
||||
message: spaces~success~${{ needs.deploy.outputs.space_url }}
|
||||
additional_text: |
|
||||
**Install Gradio from this PR**
|
||||
```bash
|
||||
pip install https://huggingface.co/buckets/gradio/pypi-previews/resolve/${{ needs.changes.outputs.sha }}/gradio-${{ needs.changes.outputs.gradio_version }}-py3-none-any.whl
|
||||
```
|
||||
|
||||
**Install Gradio Python Client from this PR**
|
||||
```bash
|
||||
pip install "gradio-client @ git+https://github.com/gradio-app/gradio@${{ needs.changes.outputs.sha }}#subdirectory=client/python"
|
||||
```
|
||||
|
||||
**Import Gradio JS Client from this PR via CDN**
|
||||
```js
|
||||
import { Client } from "https://huggingface.co/buckets/gradio/npm-previews/resolve/${{ needs.changes.outputs.sha }}/browser.js";
|
||||
```
|
||||
comment-spaces-failure:
|
||||
uses: "./.github/workflows/comment-queue.yml"
|
||||
needs: [deploy, changes]
|
||||
if: always() && needs.deploy.result == 'failure' && needs.changes.outputs.should_run == 'true'
|
||||
secrets:
|
||||
gh_token: ${{ secrets.COMMENT_TOKEN }}
|
||||
with:
|
||||
pr_number: ${{ needs.changes.outputs.pr_number }}
|
||||
message: spaces~failure~https://github.com/gradio-app/gradio/actions/runs/${{github.run_id}}/
|
||||
@@ -0,0 +1,92 @@
|
||||
# safe runs from main
|
||||
|
||||
name: publish
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
CI: true
|
||||
PNPM_CACHE_FOLDER: .pnpm-store
|
||||
jobs:
|
||||
version_or_publish:
|
||||
runs-on: ubuntu-22.04
|
||||
environment: publish
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: install dependencies
|
||||
uses: "gradio-app/gradio/.github/actions/install-all-deps@main"
|
||||
with:
|
||||
python_version: "3.12"
|
||||
skip_build: "false"
|
||||
skip_docs_gen: true
|
||||
- name: Build packages
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
pnpm package
|
||||
pnpm --filter @gradio/client build
|
||||
- name: update npm
|
||||
run: npm i -g npm@latest
|
||||
- name: check node version
|
||||
run: node -v
|
||||
- name: check npm version
|
||||
run: npm -v
|
||||
- name: create and publish versions
|
||||
id: changesets
|
||||
uses: changesets/action@aba318e9165b45b7948c60273e0b72fce0a64eb9 # @v1
|
||||
with:
|
||||
version: pnpm ci:version
|
||||
commit: "chore: update versions"
|
||||
title: "chore: update versions"
|
||||
publish: pnpm ci:publish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GRADIO_PAT }}
|
||||
- name: Upload npm logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: npm-logs
|
||||
path: |
|
||||
/home/runner/.npm/_logs/
|
||||
~/.npm/_logs/
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
- name: add label to skip chromatic build
|
||||
if: ${{ steps.changesets.outputs.pullRequestNumber != '' && steps.changesets.outputs.pullRequestNumber != 'undefined' }}
|
||||
run: gh pr edit "$PR_NUMBER" --add-label "no-visual-update"
|
||||
env:
|
||||
PR_NUMBER: ${{ steps.changesets.outputs.pullRequestNumber }}
|
||||
GITHUB_TOKEN: ${{ secrets.GRADIO_PAT }}
|
||||
- name: add label to run flaky tests
|
||||
if: ${{ steps.changesets.outputs.pullRequestNumber != '' && steps.changesets.outputs.pullRequestNumber != 'undefined' }}
|
||||
run: gh pr edit "$PR_NUMBER" --add-label "flaky-tests"
|
||||
env:
|
||||
PR_NUMBER: ${{ steps.changesets.outputs.pullRequestNumber }}
|
||||
GITHUB_TOKEN: ${{ secrets.GRADIO_PAT }}
|
||||
- name: add label to run backend tests on Windows
|
||||
if: ${{ steps.changesets.outputs.pullRequestNumber != '' && steps.changesets.outputs.pullRequestNumber != 'undefined' }}
|
||||
run: gh pr edit "$PR_NUMBER" --add-label "windows-tests"
|
||||
env:
|
||||
PR_NUMBER: ${{ steps.changesets.outputs.pullRequestNumber }}
|
||||
GITHUB_TOKEN: ${{ secrets.GRADIO_PAT }}
|
||||
- name: publish to pypi
|
||||
if: steps.changesets.outputs.hasChangesets != 'true'
|
||||
uses: "gradio-app/github/actions/publish-pypi@main"
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SDK_AWS_S3_BUCKET_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SDK_AWS_S3_BUCKET_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: us-west-2
|
||||
with:
|
||||
use-oidc: true
|
||||
- name: trigger spaces deploy workflow
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.COMMENT_TOKEN }}
|
||||
run: gh workflow run previews-build.yml
|
||||
@@ -0,0 +1,68 @@
|
||||
name: semgrep ci
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["trigger-semgrep"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
env:
|
||||
CI: true
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
|
||||
|
||||
concurrency:
|
||||
group: "${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}-${{ github.workflow_ref }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
semgrep:
|
||||
permissions:
|
||||
contents: read
|
||||
name: semgrep/ci
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: semgrep/semgrep
|
||||
options: --volume ${{ github.workspace }}/.github/configs:/mnt/ --name semgrepcontainer
|
||||
outputs:
|
||||
pr_number: ${{ steps.json.outputs.pr_number }}
|
||||
sha: ${{ steps.json.outputs.sha }}
|
||||
if: (github.actor != 'dependabot[bot]')
|
||||
steps:
|
||||
- name: Download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: changes
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
- uses: gradio-app/github/actions/json-to-output@main
|
||||
id: json
|
||||
with:
|
||||
path: output.json
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ steps.json.outputs.source_repo }}
|
||||
ref: ${{ steps.json.outputs.sha }}
|
||||
- name: restart docker
|
||||
uses: docker://docker
|
||||
with:
|
||||
args: docker restart semgrepcontainer
|
||||
- run: ls -la /mnt
|
||||
- run: semgrep ci --config=/mnt/semgrep_rules.yaml
|
||||
update-status:
|
||||
permissions:
|
||||
actions: read
|
||||
statuses: write
|
||||
runs-on: ubuntu-latest
|
||||
needs: semgrep
|
||||
steps:
|
||||
- name: update status
|
||||
uses: gradio-app/github/actions/commit-status@main
|
||||
with:
|
||||
sha: ${{ needs.semgrep.outputs.sha }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: "Semgrep Results"
|
||||
pr: ${{ needs.semgrep.outputs.pr_number }}
|
||||
result: ${{ needs.semgrep.result == 'success' && 'success' || 'failure' }}
|
||||
type: all
|
||||
@@ -0,0 +1,81 @@
|
||||
name: "storybook-build"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 5.0-dev
|
||||
|
||||
env:
|
||||
CI: true
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
|
||||
|
||||
concurrency:
|
||||
group: "${{ github.event.pull_request.number }}-${{ github.ref_name }}-${{ github.workflow }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
name: "changes"
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_run: ${{ steps.changes.outputs.should_run }}
|
||||
sha: ${{ steps.changes.outputs.sha }}
|
||||
source_branch: ${{ steps.changes.outputs.source_branch }}
|
||||
source_repo: ${{ steps.changes.outputs.source_repo }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: "gradio-app/gradio/.github/actions/changes@main"
|
||||
id: changes
|
||||
with:
|
||||
filter: "visual"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
build:
|
||||
permissions:
|
||||
contents: read
|
||||
name: :storybook-build
|
||||
runs-on: ubuntu-22.04
|
||||
needs: changes
|
||||
if: needs.changes.outputs.should_run == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.changes.outputs.source_branch }}
|
||||
repository: ${{ needs.changes.outputs.source_repo }}
|
||||
- name: install dependencies
|
||||
uses: "gradio-app/gradio/.github/actions/install-all-deps@main"
|
||||
with:
|
||||
python_version: "3.10"
|
||||
skip_build: true
|
||||
skip_docs_gen: true
|
||||
- name: build client
|
||||
run: pnpm --filter @gradio/client build
|
||||
- name: generate theme.css
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python scripts/generate_theme.py --outfile js/storybook/theme.css
|
||||
- name: cache playwright browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
playwright-${{ runner.os }}-
|
||||
- name: install playwright
|
||||
timeout-minutes: 3
|
||||
run: pnpm exec playwright install chromium --only-shell
|
||||
- name: build storybook
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
pnpm build-storybook --quiet --stats-json
|
||||
- name: upload storybook
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: storybook-static
|
||||
path: storybook-static
|
||||
@@ -0,0 +1,127 @@
|
||||
name: "storybook-deploy"
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["storybook-build"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
concurrency:
|
||||
group: "${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}-${{ github.workflow_ref }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: "changes"
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
permissions:
|
||||
actions: read
|
||||
outputs:
|
||||
should_run: ${{ steps.json.outputs.should_run }}
|
||||
sha: ${{ steps.json.outputs.sha }}
|
||||
pr_number: ${{ steps.json.outputs.pr_number }}
|
||||
source_branch: ${{ steps.json.outputs.source_branch }}
|
||||
source_repo: ${{ steps.json.outputs.source_repo }}
|
||||
labels: ${{ steps.json.outputs.labels }}
|
||||
run_id: ${{ steps.json.outputs.run_id }}
|
||||
steps:
|
||||
- name: Download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: changes
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
- uses: gradio-app/github/actions/json-to-output@main
|
||||
id: json
|
||||
with:
|
||||
path: output.json
|
||||
comment-chromatic-start:
|
||||
uses: "./.github/workflows/comment-queue.yml"
|
||||
needs: changes
|
||||
if: needs.changes.outputs.should_run == 'true' || contains(needs.changes.outputs.labels, 'no-visual-update')
|
||||
secrets:
|
||||
gh_token: ${{ secrets.COMMENT_TOKEN }}
|
||||
with:
|
||||
pr_number: ${{ needs.changes.outputs.pr_number }}
|
||||
message: |
|
||||
storybook~pending~null
|
||||
update-status:
|
||||
permissions:
|
||||
actions: read
|
||||
statuses: write
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: needs.changes.outputs.should_run == 'false' || contains(needs.changes.outputs.labels, 'no-visual-update')
|
||||
steps:
|
||||
- name: update status
|
||||
uses: gradio-app/github/actions/set-commit-status@main
|
||||
with:
|
||||
sha: ${{ needs.changes.outputs.sha }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: "UI Tests"
|
||||
run_id: ${{ needs.changes.outputs.run_id }}
|
||||
deploy:
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
environment: storybook
|
||||
name: "storybook-deploy"
|
||||
needs: changes
|
||||
if: needs.changes.outputs.should_run == 'true' && github.repository == 'gradio-app/gradio' && !contains(needs.changes.outputs.labels, 'no-visual-update')
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
changes: ${{ steps.publish-chromatic.outputs.changeCount }}
|
||||
errors: ${{ steps.publish-chromatic.outputs.errorCount }}
|
||||
storybook_url: ${{ steps.publish-chromatic.outputs.storybookUrl }}
|
||||
build_url: ${{ steps.publish-chromatic.outputs.buildUrl }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.changes.outputs.source_branch }}
|
||||
repository: ${{ needs.changes.outputs.source_repo }}
|
||||
fetch-depth: 0
|
||||
- name: dowload storybook artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: storybook-static
|
||||
path: storybook-static
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: list artifacts
|
||||
run: ls -R .
|
||||
- name: create dummy pkg.json
|
||||
run: echo "{}" > package.json
|
||||
- name: publish to chromatic
|
||||
id: publish-chromatic
|
||||
uses: chromaui/action@v13
|
||||
with:
|
||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
token: ${{ secrets.COMMIT_STATUS }}
|
||||
onlyChanged: true
|
||||
exitOnceUploaded: true
|
||||
storybookBuildDir: storybook-static
|
||||
untraced: "package.json pnpm-lock.yaml .github/** **/*.py **/*.md"
|
||||
|
||||
comment-chromatic-end:
|
||||
uses: "./.github/workflows/comment-queue.yml"
|
||||
needs: [deploy, changes]
|
||||
secrets:
|
||||
gh_token: ${{ secrets.COMMENT_TOKEN }}
|
||||
with:
|
||||
pr_number: ${{ needs.changes.outputs.pr_number }}
|
||||
message: |
|
||||
storybook~success~${{ needs.deploy.outputs.storybook_url }}
|
||||
|
||||
comment-chromatic-fail:
|
||||
uses: "./.github/workflows/comment-queue.yml"
|
||||
needs: [deploy, changes]
|
||||
if: always() && needs.deploy.result == 'failure'
|
||||
secrets:
|
||||
gh_token: ${{ secrets.COMMENT_TOKEN }}
|
||||
with:
|
||||
pr_number: ${{ needs.changes.outputs.pr_number }}
|
||||
message: |
|
||||
storybook~failure~https://github.com/gradio-app/gradio/actions/runs/${{github.run_id}}/
|
||||
@@ -0,0 +1,32 @@
|
||||
name: "sync-frontend"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- changeset-release/main
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
CI: true
|
||||
|
||||
jobs:
|
||||
sync-frontend:
|
||||
permissions:
|
||||
contents: read
|
||||
environment: deploy_spaces
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install pip
|
||||
run: python -m pip install pip wheel requests
|
||||
- name: Install Hub Client Library
|
||||
run: pip install huggingface-hub
|
||||
- name: Sync Front End Files To Hub
|
||||
run: |
|
||||
python scripts/sync_frontend.py \
|
||||
${{ secrets.SYNC_FRONTEND }}
|
||||
@@ -0,0 +1,74 @@
|
||||
name: Sync Gradio Skills to Hugging Face
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- ".agents/skills/**"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sync-skills:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout gradio repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Determine PR title
|
||||
id: pr_title
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "push" ]]; then
|
||||
echo "title=Sync Gradio Skills (${{ github.sha }})" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "title=Sync Gradio Skills (manual trigger)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Checkout skills repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: huggingface/skills
|
||||
token: ${{ secrets.GRADIO_PAT }}
|
||||
path: skills-repo
|
||||
|
||||
- name: Copy gradio skill files
|
||||
run: |
|
||||
mkdir -p skills-repo/skills/gradio/references
|
||||
cp -r .agents/skills/gradio/* skills-repo/skills/gradio/
|
||||
|
||||
- name: Copy hf-gradio skill files
|
||||
run: |
|
||||
mkdir -p skills-repo/skills/hf-gradio
|
||||
cp -r .agents/skills/hf-gradio/* skills-repo/skills/hf-gradio/
|
||||
|
||||
- name: Regenerate skills repo artifacts
|
||||
working-directory: skills-repo
|
||||
run: ./scripts/publish.sh
|
||||
|
||||
- name: Check for changes
|
||||
id: check_changes
|
||||
working-directory: skills-repo
|
||||
run: |
|
||||
git diff --quiet && echo "changed=false" >> $GITHUB_OUTPUT || echo "changed=true" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.check_changes.outputs.changed == 'true'
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GRADIO_PAT }}
|
||||
path: skills-repo
|
||||
branch: sync/gradio-skills-${{ github.run_id }}
|
||||
delete-branch: true
|
||||
title: ${{ steps.pr_title.outputs.title }}
|
||||
body: |
|
||||
Auto-generated from [gradio@${{ github.sha }}](https://github.com/gradio-app/gradio/commit/${{ github.sha }})
|
||||
|
||||
Triggered by changes to `.agents/skills/`
|
||||
|
||||
---
|
||||
This PR was created automatically by the [sync-skills](https://github.com/gradio-app/gradio/blob/main/.github/workflows/sync-skills.yml) workflow.
|
||||
commit-message: "Sync Gradio skills from gradio@${{ github.sha }}"
|
||||
labels: |
|
||||
automated
|
||||
@@ -0,0 +1,151 @@
|
||||
name: "functional"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 5.0-dev
|
||||
|
||||
concurrency:
|
||||
group: "${{ github.event.pull_request.number }}-${{ github.ref_name }}-${{ github.workflow }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
CI: true
|
||||
UV_EXCLUDE_NEWER: 7 days
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
name: "changes"
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_run: ${{ steps.changes.outputs.should_run }}
|
||||
sha: ${{ steps.changes.outputs.sha }}
|
||||
pr_number: ${{ steps.changes.outputs.pr_number }}
|
||||
source_branch: ${{ steps.changes.outputs.source_branch }}
|
||||
source_repo: ${{ steps.changes.outputs.source_repo }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: "gradio-app/gradio/.github/actions/changes@main"
|
||||
id: changes
|
||||
with:
|
||||
filter: "functional"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
build:
|
||||
name: "build-frontend"
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-20.04-16core
|
||||
needs: changes
|
||||
if: needs.changes.outputs.should_run == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# Build the frontend exactly once and share it with both SSR test legs
|
||||
# (below) via an artifact. Previously each leg rebuilt it independently,
|
||||
# paying the ~3min `pnpm build` twice. This action sets up node/pnpm,
|
||||
# installs deps and builds into `gradio/templates/**` (the SPA + the SSR
|
||||
# server bundle) — no Python/venv needed (the build only reads the
|
||||
# checked-in `gradio/package.json` for the version string).
|
||||
- name: build frontend
|
||||
uses: "gradio-app/gradio/.github/actions/install-frontend-deps@main"
|
||||
- name: upload built frontend templates
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gradio-templates
|
||||
path: gradio/templates
|
||||
retention-days: 1
|
||||
include-hidden-files: true
|
||||
test:
|
||||
# Browser legs keep their original check names (functional-test-SSR=true/
|
||||
# false); reload runs as its own leg named functional-reload.
|
||||
name: "${{ matrix.suite == 'reload' && 'functional-reload' || format('functional-test-SSR={0}', matrix.ssr) }}"
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-20.04-16core
|
||||
needs: [changes, build]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Reload tests get their own leg instead of running after the browser
|
||||
# suite inside each SSR leg: the ~99s serial reload run is taken off the
|
||||
# browser legs' critical path. Reload doesn't depend on SSR mode (the
|
||||
# reload step never set GRADIO_SSR_MODE), so it runs once here rather
|
||||
# than redundantly in both legs.
|
||||
include:
|
||||
- { suite: browser, ssr: true }
|
||||
- { suite: browser, ssr: false }
|
||||
- { suite: reload }
|
||||
if: needs.changes.outputs.should_run == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# `skip_build: true` skips the frontend build (we download it from the
|
||||
# `build` job instead). `skip_docs_gen: true` skips website JSON/CSS-var
|
||||
# doc generation (~80s, irrelevant to browser tests); it's a new input on
|
||||
# install-all-deps, so it only takes effect once this PR is merged and the
|
||||
# @main action carries it (the current @main ignores the unknown input).
|
||||
- name: install dependencies
|
||||
id: install_deps
|
||||
uses: "gradio-app/gradio/.github/actions/install-all-deps@main"
|
||||
with:
|
||||
python_version: "3.10"
|
||||
test: true
|
||||
skip_build: true
|
||||
skip_docs_gen: true
|
||||
- name: download built frontend templates
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: gradio-templates
|
||||
path: gradio/templates
|
||||
- name: verify frontend templates present
|
||||
run: |
|
||||
test -f gradio/templates/frontend/index.html \
|
||||
|| { echo "::error::frontend templates missing after artifact download"; ls -laR gradio/templates | head -50; exit 1; }
|
||||
test -d gradio/templates/node/build \
|
||||
|| { echo "::error::SSR node build missing after artifact download"; ls -laR gradio/templates | head -50; exit 1; }
|
||||
- name: cache playwright browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
playwright-${{ runner.os }}-
|
||||
- name: install playwright
|
||||
timeout-minutes: 3
|
||||
run: pnpm exec playwright install chromium --only-shell
|
||||
- name: build essential packages
|
||||
run: pnpm --filter @gradio/utils --filter @gradio/theme package
|
||||
- name: run browser tests
|
||||
if: matrix.suite == 'browser'
|
||||
env:
|
||||
GRADIO_SSR_MODE: ${{ matrix.ssr }}
|
||||
# 16-core runner + largely wait-bound tests → parallelize beyond the
|
||||
# historical default of 4 workers (see .config/playwright.config.js).
|
||||
PW_BROWSER_WORKERS: 8
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
pnpm test:browser
|
||||
- name: upload screenshots
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always() && matrix.suite == 'browser'
|
||||
with:
|
||||
name: playwright-screenshots-SSR-${{ matrix.ssr }}
|
||||
path: |
|
||||
./test-results
|
||||
- name: run reload mode test
|
||||
if: matrix.suite == 'reload'
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
pnpm test:browser:reload
|
||||
- name: upload reload mode screenshots
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always() && matrix.suite == 'reload'
|
||||
with:
|
||||
name: reload-mode-playwright-screenshots
|
||||
path: |
|
||||
./test-results
|
||||
@@ -0,0 +1,33 @@
|
||||
name: "hygiene"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 5.0-dev
|
||||
|
||||
concurrency:
|
||||
group: "${{ github.event.pull_request.number }}-${{ github.ref_name }}-${{ github.workflow }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
permissions:
|
||||
contents: read
|
||||
name: "hygiene-test"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Generate Skill
|
||||
run: |
|
||||
python3 -m venv venv
|
||||
. venv/bin/activate
|
||||
pip install -e client/python . && python scripts/generate_skill.py --check
|
||||
- name: Check for large files
|
||||
uses: actionsdesk/lfs-warning@4b98a8a5e6c429c23c34eee02d71553bca216425 # @v3.3
|
||||
with:
|
||||
filesizelimit: 5MB
|
||||
exclusionPatterns: ffmpeg-bin/*
|
||||
@@ -0,0 +1,161 @@
|
||||
name: "python"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, edited, labeled, unlabeled]
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: "${{ github.event.pull_request.number }}-${{ github.ref_name }}-${{ github.workflow }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
HF_TOKEN: ${{ vars.HF_TOKEN }}
|
||||
UV_EXCLUDE_NEWER: 7 days
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
name: "changes"
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_run: ${{ steps.changes.outputs.should_run }}
|
||||
sha: ${{ steps.changes.outputs.sha }}
|
||||
pr_number: ${{ steps.changes.outputs.pr_number }}
|
||||
source_branch: ${{ steps.changes.outputs.source_branch }}
|
||||
source_repo: ${{ steps.changes.outputs.source_repo }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: "gradio-app/gradio/.github/actions/changes@main"
|
||||
id: changes
|
||||
with:
|
||||
filter: "gradio"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
build:
|
||||
permissions:
|
||||
contents: read
|
||||
name: "build"
|
||||
needs: changes
|
||||
if: needs.changes.outputs.should_run == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: install dependencies
|
||||
id: install_deps
|
||||
uses: "gradio-app/gradio/.github/actions/install-all-deps@main"
|
||||
with:
|
||||
python_version: "3.10"
|
||||
os: "ubuntu-latest"
|
||||
test: true
|
||||
skip_docs_gen: true
|
||||
- name: Lint
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
./scripts/lint_backend.sh
|
||||
- name: Typecheck
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
./scripts/type_check_backend.sh
|
||||
- name: Build wheel
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
uv pip install build
|
||||
python -m build
|
||||
- name: Build gradio_client wheel
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
cd client/python
|
||||
python -m build
|
||||
- name: Upload gradio wheel
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gradio-wheel
|
||||
path: dist/*.whl
|
||||
- name: Upload gradio_client wheel
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gradio-client-wheel
|
||||
path: client/python/dist/*.whl
|
||||
test:
|
||||
permissions:
|
||||
contents: read
|
||||
name: "test-${{ matrix.os }}-${{ matrix.test-type == 'flaky' && 'flaky' || 'not-flaky'}}"
|
||||
needs: [changes, build]
|
||||
if: needs.changes.outputs.should_run == 'true'
|
||||
strategy:
|
||||
matrix:
|
||||
os: ["ubuntu-latest", "windows-latest"]
|
||||
test-type: ["not flaky", "flaky"]
|
||||
exclude:
|
||||
- os: ${{ github.event_name == 'pull_request' && contains( github.event.pull_request.labels.*.name, 'windows-tests') && 'dummy' || 'windows-latest' }}
|
||||
- test-type: ${{ github.event_name == 'pull_request' && contains( github.event.pull_request.labels.*.name, 'flaky-tests') && 'dummy' || 'flaky' }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Download gradio wheel
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: gradio-wheel
|
||||
path: dist/
|
||||
- name: Download gradio_client wheel
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: gradio-client-wheel
|
||||
path: client-dist/
|
||||
- name: install dependencies
|
||||
id: install_deps
|
||||
uses: "gradio-app/gradio/.github/actions/install-all-deps@main"
|
||||
with:
|
||||
python_version: "3.10"
|
||||
os: ${{ matrix.os }}
|
||||
test: true
|
||||
skip_build: true
|
||||
skip_gradio_install: true
|
||||
skip_docs_gen: true
|
||||
- name: Install gradio_client wheel
|
||||
shell: bash
|
||||
run: |
|
||||
. ${{steps.install_deps.outputs.venv_activate}}
|
||||
uv pip install client-dist/*.whl
|
||||
- name: Install gradio wheel
|
||||
shell: bash
|
||||
run: |
|
||||
. ${{steps.install_deps.outputs.venv_activate}}
|
||||
uv pip install "$(ls dist/*.whl)[oauth,mcp]"
|
||||
- name: uv pip freeze
|
||||
shell: bash
|
||||
run: |
|
||||
. ${{steps.install_deps.outputs.venv_activate}}
|
||||
uv pip freeze
|
||||
- name: Generate .pyi files
|
||||
shell: bash
|
||||
run: |
|
||||
. ${{steps.install_deps.outputs.venv_activate}}
|
||||
python -c "import gradio"
|
||||
- name: Copy wheels for docker tests
|
||||
shell: bash
|
||||
run: |
|
||||
cp dist/*.whl test/test_docker/
|
||||
cp client-dist/*.whl test/test_docker/
|
||||
- name: Remove local gradio source to use installed wheel
|
||||
shell: bash
|
||||
run: |
|
||||
find gradio -name "__init__.py" -delete
|
||||
- name: Run parallel tests
|
||||
shell: bash
|
||||
run: |
|
||||
. ${{steps.install_deps.outputs.venv_activate}}
|
||||
python -m pytest -n auto -m "${{ matrix.test-type }} and not serial"
|
||||
- name: Run non-flaky serial tests
|
||||
if: matrix.test-type == 'not flaky'
|
||||
shell: bash
|
||||
run: |
|
||||
. ${{steps.install_deps.outputs.venv_activate}}
|
||||
python -m pytest -m "${{ matrix.test-type }} and serial"
|
||||
@@ -0,0 +1,72 @@
|
||||
name: "js"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 5.0-dev
|
||||
|
||||
env:
|
||||
CI: true
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
|
||||
|
||||
concurrency:
|
||||
group: "${{ github.event.pull_request.number }}-${{ github.ref_name }}-${{ github.workflow }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
name: "changes"
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_run: ${{ steps.changes.outputs.should_run }}
|
||||
sha: ${{ steps.changes.outputs.sha }}
|
||||
pr_number: ${{ steps.changes.outputs.pr_number }}
|
||||
source_branch: ${{ steps.changes.outputs.source_branch }}
|
||||
source_repo: ${{ steps.changes.outputs.source_repo }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: "gradio-app/gradio/.github/actions/changes@main"
|
||||
id: changes
|
||||
with:
|
||||
filter: "js"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test:
|
||||
permissions:
|
||||
contents: read
|
||||
name: js-test
|
||||
runs-on: ubuntu-22.04
|
||||
needs: changes
|
||||
if: needs.changes.outputs.should_run == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: install dependencies
|
||||
uses: "gradio-app/gradio/.github/actions/install-frontend-deps@main"
|
||||
with:
|
||||
skip_build: true
|
||||
- name: cache playwright browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
playwright-${{ runner.os }}-
|
||||
- name: install playwright
|
||||
timeout-minutes: 3
|
||||
run: pnpm exec playwright install chromium --only-shell
|
||||
- name: format check
|
||||
run: pnpm format:check
|
||||
- name: lint
|
||||
run: pnpm lint
|
||||
- name: typecheck
|
||||
run: pnpm ts:check
|
||||
- name: unit tests
|
||||
run: pnpm test:run
|
||||
- name: client tests
|
||||
run: pnpm --filter @gradio/client test
|
||||
@@ -0,0 +1,17 @@
|
||||
name: trigger-changeset
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, edited, labeled, unlabeled]
|
||||
branches:
|
||||
- main
|
||||
issue_comment:
|
||||
types: [edited]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
changeset:
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event.sender.login != 'gradio-pr-bot'
|
||||
steps:
|
||||
- run: echo "Requesting changeset"
|
||||
@@ -0,0 +1,29 @@
|
||||
name: trigger-semgrep
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- 5.0-dev
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
name: "changes"
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_run: ${{ steps.changes.outputs.should_run }}
|
||||
sha: ${{ steps.changes.outputs.sha }}
|
||||
pr_number: ${{ steps.changes.outputs.pr_number }}
|
||||
source_branch: ${{ steps.changes.outputs.source_branch }}
|
||||
source_repo: ${{ steps.changes.outputs.source_repo }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: "gradio-app/gradio/.github/actions/changes@main"
|
||||
id: changes
|
||||
with:
|
||||
filter: "gradio"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,68 @@
|
||||
name: "update-checks"
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["python", "js", "functional", "generate-changeset"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
concurrency:
|
||||
group: "${{ github.event.workflow_run.id}}"
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: "changes"
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
outputs:
|
||||
should_run: ${{ steps.json.outputs.should_run }}
|
||||
sha: ${{ steps.json.outputs.sha }}
|
||||
pr_number: ${{ steps.json.outputs.pr_number }}
|
||||
source_branch: ${{ steps.json.outputs.source_branch }}
|
||||
source_repo: ${{ steps.json.outputs.source_repo }}
|
||||
labels: ${{ steps.json.outputs.labels }}
|
||||
run_id: ${{ steps.json.outputs.run_id }}
|
||||
steps:
|
||||
- name: Download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: changes
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
- uses: gradio-app/github/actions/json-to-output@main
|
||||
id: json
|
||||
with:
|
||||
path: output.json
|
||||
update-status:
|
||||
permissions:
|
||||
actions: read
|
||||
statuses: write
|
||||
contents: read
|
||||
environment: commit_status
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.should_run == 'false'}}
|
||||
steps:
|
||||
- name: set js check name
|
||||
if: github.event.workflow_run.name == 'js'
|
||||
run: echo "CHECK_NAME=js / js-test (pull_request)" >> $GITHUB_ENV
|
||||
- name: set python check name
|
||||
if: github.event.workflow_run.name == 'python'
|
||||
run: echo "CHECK_NAME=test-ubuntu-latest-not-flaky" >> $GITHUB_ENV
|
||||
- name: set functional check name
|
||||
if: github.event.workflow_run.name == 'functional'
|
||||
run: echo "CHECK_NAME=functional / functional-test (pull_request)" >> $GITHUB_ENV
|
||||
- name: set changeset approval status
|
||||
if: github.event.workflow_run.name == 'generate-changeset'
|
||||
run: echo "CHECK_NAME=changeset-approval (pull_request)" >> $GITHUB_ENV
|
||||
- name: update status
|
||||
uses: gradio-app/github/actions/set-commit-status@main
|
||||
with:
|
||||
sha: ${{ needs.changes.outputs.sha }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: ${{ env.CHECK_NAME }}
|
||||
run_id: ${{ needs.changes.outputs.run_id }}
|
||||
@@ -0,0 +1,103 @@
|
||||
name: "website-build"
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["docs-deploy"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
env:
|
||||
CI: true
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
name: "changes"
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
outputs:
|
||||
should_run: ${{ steps.json.outputs.should_run }}
|
||||
sha: ${{ steps.json.outputs.sha }}
|
||||
pr_number: ${{ steps.json.outputs.pr_number }}
|
||||
source_branch: ${{ steps.json.outputs.source_branch }}
|
||||
source_repo: ${{ steps.json.outputs.source_repo }}
|
||||
labels: ${{ steps.json.outputs.labels }}
|
||||
run_id: ${{ steps.json.outputs.run_id }}
|
||||
steps:
|
||||
- name: Download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: changes
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
- uses: gradio-app/github/actions/json-to-output@main
|
||||
id: json
|
||||
with:
|
||||
path: output.json
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: output.json
|
||||
name: changes
|
||||
build:
|
||||
environment: build_website
|
||||
name: "website-build"
|
||||
runs-on: ubuntu-22.04
|
||||
needs: changes
|
||||
if: needs.changes.outputs.should_run == 'true' || (endsWith(needs.changes.outputs.source_branch, 'main') && github.repository == 'gradio-app/gradio')
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.changes.outputs.source_branch }}
|
||||
repository: ${{ needs.changes.outputs.source_repo }}
|
||||
- name: install dependencies
|
||||
uses: "gradio-app/gradio/.github/actions/install-all-deps@main"
|
||||
with:
|
||||
skip_build: true
|
||||
python_version: "3.10"
|
||||
- name: build client
|
||||
run: pnpm --filter @gradio/client build
|
||||
|
||||
- name: build packages
|
||||
run: pnpm package
|
||||
|
||||
- name: build website
|
||||
run: VERCEL=1 pnpm --filter website build
|
||||
|
||||
- name: find
|
||||
run: find . -type d
|
||||
|
||||
- name: Install Hub Client Library and docs.json Requirements
|
||||
run: |
|
||||
python3 -m venv venv
|
||||
. venv/bin/activate
|
||||
pip install huggingface-hub==0.28.1 html2text beautifulsoup4
|
||||
|
||||
- name: Get docs.json Upload Script
|
||||
run: |
|
||||
mkdir -p scripts
|
||||
curl https://raw.githubusercontent.com/gradio-app/gradio/main/scripts/upload_docs_json.py -o scripts/upload_docs_json.py
|
||||
|
||||
- name: Upload docs.json to HF dataset
|
||||
if: needs.changes.outputs.source_repo == 'gradio-app/gradio' && needs.changes.outputs.source_branch == 'refs/heads/main'
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_DOCS_TOKEN }}
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python scripts/upload_docs_json.py
|
||||
- name: upload website artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: website
|
||||
path: |
|
||||
js/_website/build
|
||||
js/_website/wrangler.jsonc
|
||||
js/_website/worker
|
||||
include-hidden-files: true
|
||||
- name: upload functions artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: functions
|
||||
path: js/_website/functions
|
||||
include-hidden-files: true
|
||||
@@ -0,0 +1,106 @@
|
||||
name: "website-deploy"
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["website-build"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
name: "changes"
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
outputs:
|
||||
should_run: ${{ steps.json.outputs.should_run }}
|
||||
sha: ${{ steps.json.outputs.sha }}
|
||||
pr_number: ${{ steps.json.outputs.pr_number }}
|
||||
source_branch: ${{ steps.json.outputs.source_branch }}
|
||||
source_repo: ${{ steps.json.outputs.source_repo }}
|
||||
labels: ${{ steps.json.outputs.labels }}
|
||||
run_id: ${{ steps.json.outputs.run_id }}
|
||||
steps:
|
||||
- name: Download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: changes
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
- uses: gradio-app/github/actions/json-to-output@main
|
||||
id: json
|
||||
with:
|
||||
path: output.json
|
||||
comment-deploy-start:
|
||||
needs: changes
|
||||
uses: "./.github/workflows/comment-queue.yml"
|
||||
if: needs.changes.outputs.should_run == 'true'
|
||||
secrets:
|
||||
gh_token: ${{ secrets.COMMENT_TOKEN }}
|
||||
with:
|
||||
pr_number: ${{ needs.changes.outputs.pr_number }}
|
||||
message: website~pending~null
|
||||
deploy:
|
||||
environment: deploy_website
|
||||
name: "website-deploy"
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: needs.changes.outputs.should_run == 'true' || (endsWith(needs.changes.outputs.source_branch, 'main') && github.repository == 'gradio-app/gradio')
|
||||
permissions:
|
||||
actions: read
|
||||
outputs:
|
||||
cloudflare_url: ${{ steps.cloudflare-preview.outputs.deployment-url }}
|
||||
steps:
|
||||
- name: download website artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: website
|
||||
path: js/_website
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# preview
|
||||
- name: Deploy Preview Worker to Cloudflare
|
||||
if: needs.changes.outputs.pr_number != 'false'
|
||||
id: cloudflare-preview
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
workingDirectory: js/_website
|
||||
wranglerVersion: "4"
|
||||
command: versions upload
|
||||
|
||||
# production
|
||||
- name: Deploy Production Worker to Cloudflare
|
||||
if: needs.changes.outputs.source_repo == 'gradio-app/gradio' && needs.changes.outputs.source_branch == 'refs/heads/main'
|
||||
id: cloudflare-production
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
workingDirectory: js/_website
|
||||
wranglerVersion: "4"
|
||||
command: deploy
|
||||
|
||||
comment-deploy-success:
|
||||
uses: "./.github/workflows/comment-queue.yml"
|
||||
needs: [deploy, changes]
|
||||
if: needs.deploy.result == 'success' && needs.changes.outputs.pr_number != 'false'
|
||||
secrets:
|
||||
gh_token: ${{ secrets.COMMENT_TOKEN }}
|
||||
with:
|
||||
pr_number: ${{ needs.changes.outputs.pr_number }}
|
||||
message: website~success~${{needs.deploy.outputs.cloudflare_url}}
|
||||
comment-deploy-failure:
|
||||
uses: "./.github/workflows/comment-queue.yml"
|
||||
needs: [deploy, changes]
|
||||
if: always() && needs.deploy.result == 'failure' && needs.changes.outputs.pr_number != 'false'
|
||||
secrets:
|
||||
gh_token: ${{ secrets.COMMENT_TOKEN }}
|
||||
with:
|
||||
pr_number: ${{ needs.changes.outputs.pr_number }}
|
||||
message: website~failure~https://github.com/gradio-app/gradio/actions/runs/${{github.run_id}}/
|
||||
@@ -0,0 +1,82 @@
|
||||
name: "docs-build"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
CI: true
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
|
||||
|
||||
concurrency:
|
||||
group: "${{ github.event.pull_request.number }}-${{ github.ref_name }}-${{ github.workflow }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
if: github.event_name == 'pull_request' || github.event_name == 'push'
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
name: "changes"
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_run: ${{ steps.changes.outputs.should_run }}
|
||||
sha: ${{ steps.changes.outputs.sha }}
|
||||
gradio_version: ${{ steps.changes.outputs.gradio_version }}
|
||||
source_branch: ${{ steps.changes.outputs.source_branch }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: "gradio-app/gradio/.github/actions/changes@main"
|
||||
id: changes
|
||||
with:
|
||||
filter: "website"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build:
|
||||
permissions:
|
||||
contents: read
|
||||
name: "docs-build"
|
||||
runs-on: ubuntu-22.04
|
||||
needs: changes
|
||||
if: needs.changes.outputs.should_run == 'true' || (endsWith(needs.changes.outputs.source_branch, 'main') && github.repository == 'gradio-app/gradio')
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: install dependencies
|
||||
uses: "gradio-app/gradio/.github/actions/install-all-deps@main"
|
||||
with:
|
||||
python_version: "3.10"
|
||||
skip_build: true
|
||||
test: true
|
||||
|
||||
# generated when installing deps
|
||||
- name: upload website json artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: website-json
|
||||
path: js/_website/src/lib/json
|
||||
- name: upload website json templates
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: website-templates
|
||||
path: js/_website/src/lib/templates
|
||||
- name: build client
|
||||
run: pnpm --filter @gradio/client build
|
||||
- name: build packages
|
||||
run: pnpm package
|
||||
- name: build website
|
||||
run: VERCEL=1 pnpm --filter website build
|
||||
website-build:
|
||||
permissions:
|
||||
contents: read
|
||||
name: "website-build"
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [changes, build]
|
||||
if: needs.changes.outputs.should_run == 'true' || (endsWith(needs.changes.outputs.source_branch, 'main') && github.repository == 'gradio-app/gradio')
|
||||
steps:
|
||||
- name: website build completed
|
||||
run: echo "Website build completed in the docs-build job."
|
||||
@@ -0,0 +1,93 @@
|
||||
name: "docs-deploy"
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["docs-build"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
concurrency:
|
||||
group: "${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}-${{ github.workflow_ref }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
name: "changes"
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
outputs:
|
||||
should_run: ${{ steps.json.outputs.should_run }}
|
||||
sha: ${{ steps.json.outputs.sha }}
|
||||
pr_number: ${{ steps.json.outputs.pr_number }}
|
||||
source_branch: ${{ steps.json.outputs.source_branch }}
|
||||
source_repo: ${{ steps.json.outputs.source_repo }}
|
||||
labels: ${{ steps.json.outputs.labels }}
|
||||
run_id: ${{ steps.json.outputs.run_id }}
|
||||
steps:
|
||||
- name: Download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: changes
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
- uses: gradio-app/github/actions/json-to-output@main
|
||||
id: json
|
||||
with:
|
||||
path: output.json
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: output.json
|
||||
name: changes
|
||||
comment-deploy-start:
|
||||
needs: changes
|
||||
uses: "./.github/workflows/comment-queue.yml"
|
||||
if: needs.changes.outputs.should_run == 'true'
|
||||
secrets:
|
||||
gh_token: ${{ secrets.COMMENT_TOKEN }}
|
||||
with:
|
||||
pr_number: ${{ needs.changes.outputs.pr_number }}
|
||||
message: website~pending~null
|
||||
deploy:
|
||||
environment: deploy_website
|
||||
name: "docs-deploy"
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: needs.changes.outputs.should_run == 'true' || github.event.workflow_run.event == 'push'
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: website-json
|
||||
path: js/_website/lib/json
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: website-templates
|
||||
path: js/_website/lib/templates
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: deploy json to aws
|
||||
if: startsWith(needs.changes.outputs.source_branch, 'changeset-release/') && needs.changes.outputs.source_repo == 'gradio-app/gradio'
|
||||
run: |
|
||||
export AWS_ACCESS_KEY_ID=${{ secrets.DOCS_JSON_AWS_S3_ACCESS_KEY }}
|
||||
export AWS_SECRET_ACCESS_KEY=${{ secrets.DOCS_JSON_AWS_S3_SECRET_ACCESS_KEY }}
|
||||
export AWS_DEFAULT_REGION=us-west-2
|
||||
version=$(jq -r .version js/_website/lib/json/version.json)
|
||||
aws s3 cp ./js/_website/lib/json/ s3://gradio-docs-json/$version/ --recursive
|
||||
aws s3 cp ./js/_website/lib/templates/ s3://gradio-docs-json/$version/templates/ --recursive
|
||||
comment-deploy-failure:
|
||||
uses: "./.github/workflows/comment-queue.yml"
|
||||
needs: [deploy, changes]
|
||||
if: always() && needs.deploy.result == 'failure' && needs.changes.outputs.pr_number != 'false'
|
||||
secrets:
|
||||
gh_token: ${{ secrets.COMMENT_TOKEN }}
|
||||
with:
|
||||
pr_number: ${{ needs.changes.outputs.pr_number }}
|
||||
message: website~failure~https://github.com/gradio-app/gradio/actions/runs/${{github.run_id}}/
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
# Python build
|
||||
.eggs/
|
||||
gradio.egg-info
|
||||
dist/
|
||||
*.pyc
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
build/
|
||||
!js/build/
|
||||
!js/build/dist/
|
||||
__tmp/*
|
||||
*.pyi
|
||||
!gradio/stubs/**/*.pyi
|
||||
.ipynb_checkpoints/
|
||||
.python-version
|
||||
=23.2
|
||||
|
||||
# JS build
|
||||
gradio/templates/*
|
||||
gradio/node/*
|
||||
gradio/_frontend_code/*
|
||||
js/gradio-preview/test/*
|
||||
**/dist/**/*.d.ts
|
||||
|
||||
# Secrets
|
||||
.env
|
||||
|
||||
# Gradio run artifacts
|
||||
*.db
|
||||
*.sqlite3
|
||||
gradio/launches.json
|
||||
gradio/hash_seed.txt
|
||||
.gradio/
|
||||
|
||||
tmp.zip
|
||||
|
||||
# Tests
|
||||
.coverage
|
||||
coverage.xml
|
||||
test.txt
|
||||
**/snapshots/**/*.png
|
||||
playwright-report/
|
||||
.hypothesis
|
||||
|
||||
# Demos
|
||||
demo/tmp.zip
|
||||
demo/files/*.avi
|
||||
demo/files/*.mp4
|
||||
demo/all_demos/demos/*
|
||||
demo/all_demos/requirements.txt
|
||||
demo/*/config.json
|
||||
demo/annotatedimage_component/*.png
|
||||
demo/fake_diffusion_with_gif/*.gif
|
||||
demo/cancel_events/cancel_events_output_log.txt
|
||||
demo/unload_event_test/output_log.txt
|
||||
demo/stream_video_out/output_*.ts
|
||||
demo/stream_video_out/output_*.mp4
|
||||
demo/stream_audio_out/*.mp3
|
||||
test-demo/*
|
||||
#demo/image_editor_story/*.png
|
||||
|
||||
# Etc
|
||||
.idea/*
|
||||
.DS_Store
|
||||
*.bak
|
||||
workspace.code-workspace
|
||||
*.h5
|
||||
|
||||
# dev containers
|
||||
.pnpm-store/
|
||||
|
||||
# log files
|
||||
.pnpm-debug.log
|
||||
|
||||
# Local virtualenv for devs
|
||||
.venv*
|
||||
|
||||
# FRP
|
||||
gradio/frpc_*
|
||||
.vercel
|
||||
|
||||
# js
|
||||
node_modules
|
||||
public/build/
|
||||
test-results
|
||||
client/js/dist/*
|
||||
client/js/test.js
|
||||
.config/test.py
|
||||
.svelte-kit
|
||||
|
||||
|
||||
# storybook
|
||||
storybook-static
|
||||
build-storybook.log
|
||||
js/storybook/theme.css
|
||||
*storybook.log
|
||||
|
||||
# playwright
|
||||
.config/playwright/.cache
|
||||
|
||||
*.local.*
|
||||
.claude/settings.local
|
||||
CLAUDE.md
|
||||
.sonda/
|
||||
|
||||
# docs upload
|
||||
scripts/docs.json
|
||||
tmp_input
|
||||
.claude/scheduled_tasks.lock
|
||||
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { setContext } from "svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
import { init, addMessages } from "svelte-i18n";
|
||||
import en from "../js/core/src/lang/en.json";
|
||||
|
||||
const GRADIO_ROOT = "GRADIO_ROOT";
|
||||
|
||||
let { children }: { children: Snippet } = $props();
|
||||
let i18nReady = $state(false);
|
||||
|
||||
addMessages("en", en);
|
||||
init({
|
||||
fallbackLocale: "en",
|
||||
initialLocale: "en"
|
||||
});
|
||||
i18nReady = true;
|
||||
|
||||
const mockRegister = (): void => {};
|
||||
|
||||
const mockDispatcher = (_id: number, event: string, data: any): void => {
|
||||
const e = new CustomEvent("gradio", {
|
||||
bubbles: true,
|
||||
detail: { data, id: _id, event }
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
};
|
||||
|
||||
setContext(GRADIO_ROOT, {
|
||||
register: mockRegister,
|
||||
dispatcher: mockDispatcher
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if i18nReady}
|
||||
{@render children()}
|
||||
{/if}
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { StorybookConfig } from "@storybook/svelte-vite";
|
||||
import { svelte, vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||
import { sveltePreprocess } from "svelte-preprocess";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: [
|
||||
"../js/storybook/**/*.mdx",
|
||||
"../js/!(dataframe|dataframe-interim|core)/*.stories.svelte",
|
||||
"../js/!(dataframe|dataframe-interim|core)/src/**/*.stories.svelte"
|
||||
],
|
||||
staticDirs: ["../js/spa/public", "../js/storybook/public", "./test_files"],
|
||||
addons: [
|
||||
{
|
||||
name: "@storybook/addon-svelte-csf",
|
||||
options: {
|
||||
legacyTemplate: true
|
||||
}
|
||||
},
|
||||
"@chromatic-com/storybook",
|
||||
"@storybook/addon-vitest",
|
||||
"@storybook/addon-a11y",
|
||||
"@storybook/addon-docs"
|
||||
],
|
||||
framework: "@storybook/svelte-vite",
|
||||
async viteFinal(config) {
|
||||
config.plugins = (config.plugins || []).filter((plugin) => {
|
||||
const name = Array.isArray(plugin) ? undefined : (plugin as any)?.name;
|
||||
return (
|
||||
name !== "storybook:svelte-docgen-plugin" &&
|
||||
name !== "vite-plugin-svelte"
|
||||
);
|
||||
});
|
||||
|
||||
config.plugins.unshift(
|
||||
svelte({
|
||||
configFile: false,
|
||||
// configFile:false means the project's svelte.config.js (and its
|
||||
// preprocessing) isn't loaded, so enable TS preprocessing here —
|
||||
// matching the main app's config. Without it, Svelte's built-in
|
||||
// parser chokes on TS generics in a class `extends` clause
|
||||
// (e.g. `extends Gradio<A, B>`).
|
||||
preprocess: [vitePreprocess(), sveltePreprocess()],
|
||||
// Disabled so workspace @gradio/* .svelte sources go through this
|
||||
// (preprocessed) plugin rather than being esbuild-prebundled without
|
||||
// preprocessing — the prebundle path can't strip TS generics in a
|
||||
// class `extends` clause and fails to parse on Svelte 5.48.
|
||||
prebundleSvelteLibraries: false,
|
||||
dynamicCompileOptions({ filename }) {
|
||||
// Process @storybook/svelte files from node_modules
|
||||
if (filename.includes("node_modules/@storybook/svelte")) {
|
||||
return { generate: "client" };
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
config.resolve = config.resolve || {};
|
||||
config.resolve.conditions = [
|
||||
"gradio",
|
||||
"browser",
|
||||
"import",
|
||||
"module",
|
||||
"default"
|
||||
];
|
||||
|
||||
config.optimizeDeps = config.optimizeDeps || {};
|
||||
config.optimizeDeps.include = [
|
||||
...(config.optimizeDeps.include || []),
|
||||
"@storybook/svelte"
|
||||
];
|
||||
config.optimizeDeps.exclude = [
|
||||
...(config.optimizeDeps.exclude || []),
|
||||
"@gradio/utils",
|
||||
"@gradio/theme"
|
||||
];
|
||||
|
||||
return config;
|
||||
}
|
||||
};
|
||||
export default config;
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { Preview } from "@storybook/svelte-vite";
|
||||
import GradioContext from "./GradioContext.svelte";
|
||||
import { setupi18n } from "../js/core/src/i18n";
|
||||
import { locale } from "svelte-i18n";
|
||||
|
||||
import "../js/theme/src/reset.css";
|
||||
import "../js/theme/src/global.css";
|
||||
import "../js/theme/src/pollen.css";
|
||||
import "../js/theme/src/typography.css";
|
||||
import "../js/storybook/theme.css";
|
||||
|
||||
setupi18n();
|
||||
|
||||
const withI18n = (
|
||||
storyFn: any,
|
||||
context: { globals: { locale?: string } }
|
||||
): any => {
|
||||
if (context.globals.locale) {
|
||||
locale.set(context.globals.locale);
|
||||
}
|
||||
return storyFn();
|
||||
};
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i
|
||||
}
|
||||
},
|
||||
|
||||
a11y: {
|
||||
// 'todo' - show a11y violations in the test UI only
|
||||
test: "todo"
|
||||
},
|
||||
viewport: {
|
||||
viewports: {
|
||||
mobile: { name: "mobile", styles: { width: "320px", height: "400px" } },
|
||||
tablet: { name: "tablet", styles: { width: "640px", height: "800px" } },
|
||||
desktop: {
|
||||
name: "desktop",
|
||||
styles: { width: "1024px", height: "1000px" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
globalTypes: {
|
||||
locale: {
|
||||
name: "Locale",
|
||||
description: "Internationalization locale",
|
||||
defaultValue: "en",
|
||||
toolbar: {
|
||||
icon: "globe",
|
||||
items: [
|
||||
{ value: "en", title: "English" },
|
||||
{ value: "fr", title: "French" },
|
||||
{ value: "de", title: "German" },
|
||||
{ value: "es", title: "Spanish" },
|
||||
{ value: "ar", title: "Arabic", right: true }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
decorators: [
|
||||
withI18n,
|
||||
() => ({
|
||||
Component: GradioContext
|
||||
})
|
||||
]
|
||||
};
|
||||
|
||||
export default preview;
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
import { setProjectAnnotations } from "@storybook/svelte";
|
||||
import * as previewAnnotations from "./preview";
|
||||
|
||||
setProjectAnnotations([previewAnnotations]);
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"phoenisx.cssvar",
|
||||
"esbenp.prettier-vscode",
|
||||
"svelte.svelte-vscode",
|
||||
"charliermarsh.ruff"
|
||||
]
|
||||
}
|
||||
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"cssvar.files": ["./js/node_modules/pollen-css/pollen.css"],
|
||||
"cssvar.ignore": [],
|
||||
"cssvar.disableSort": true,
|
||||
"cssvar.extensions": ["js", "css", "html", "jsx", "tsx", "svelte"],
|
||||
"svelte.plugin.svelte.format.enable": true,
|
||||
"svelte.plugin.svelte.diagnostics.enable": false,
|
||||
"svelte.enable-ts-plugin": true,
|
||||
"prettier.configPath": ".config/.prettierrc.json",
|
||||
"prettier.ignorePath": ".config/.prettierignore",
|
||||
"python.testing.pytestArgs": ["."],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"eslint.validate": ["javascript", "typescript", "html", "markdown", "svelte"],
|
||||
"eslint.useFlatConfig": true,
|
||||
"eslint.options": {
|
||||
"overrideConfigFile": "./.config/eslint.config.js"
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"i18n-ally.localesPaths": [
|
||||
"js/spa/src/lang"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
# AGENTS.md
|
||||
|
||||
Instructions for AI coding agents working on this repository.
|
||||
|
||||
## Repository Structure
|
||||
|
||||
- `gradio/` — Python source for the Gradio library (backend)
|
||||
- `gradio/components/` — all Gradio components
|
||||
- `gradio/cli/` — CLI commands (`gradio`, `gradio cc`, `gradio skills`, etc.)
|
||||
- `client/python/` — the `gradio_client` Python client library
|
||||
- `client/js/` — the `@gradio/client` JavaScript client library
|
||||
- `js/` — frontend code (Svelte/TypeScript), with each component in its own subdirectory
|
||||
- `test/` — Python backend tests (pytest)
|
||||
- `js/spa/test/` — browser/Playwright tests (`*.spec.ts`)
|
||||
- `demo/` — example Gradio apps
|
||||
- `guides/` — written guides and tutorials for the website
|
||||
|
||||
## Pull Request Rules
|
||||
|
||||
Follow these rules when creating or contributing to pull requests:
|
||||
|
||||
1. **Target an issue.** Every non-trivial PR should reference an existing GitHub issue. If one doesn't exist, create it first. PRs without a linked issue may be closed.
|
||||
|
||||
2. **Use the PR template.** Fill out every section of `.github/PULL_REQUEST_TEMPLATE.md`, including:
|
||||
- A clear description of the change
|
||||
- The AI disclosure checkbox (see below)
|
||||
- The linked issue (`Closes: #NNN`)
|
||||
|
||||
3. **AI disclosure is mandatory.** If AI was used in any non-trivial way (drafting code, writing the PR description, etc.), you must disclose this in the PR template. Trivial autocomplete does not need to be disclosed. All AI-generated code must be self-reviewed.
|
||||
|
||||
4. **Format your code before pushing.**
|
||||
- Backend: `bash scripts/format_backend.sh`
|
||||
- Frontend: `bash scripts/format_frontend.sh`
|
||||
|
||||
5. **Tests must pass.** PRs are only merged when CI is green. Run backend tests locally with `bash scripts/run_backend_tests.sh`.
|
||||
|
||||
6. **PR title and description should be clear and written in English.** The title should concisely describe *what* the PR does. The description should explain *why*.
|
||||
|
||||
7. **Submit against `main`.** All PRs target the `main` branch.
|
||||
|
||||
## Code Style
|
||||
|
||||
- Python code is formatted with `ruff`. Run `bash scripts/format_backend.sh`.
|
||||
- Frontend code is formatted with `prettier`. Run `bash scripts/format_frontend.sh`.
|
||||
- Be consistent with the style of the surrounding code.
|
||||
|
||||
## Agentic Contribution Policy
|
||||
|
||||
These rules apply to all AI-assisted contributions to this repository. Do not skip them.
|
||||
|
||||
### Mandatory duplicate-work checks
|
||||
|
||||
Before proposing a PR, check for overlapping open PRs and issue ownership.
|
||||
|
||||
- If an open PR already addresses the same fix, do not open another.
|
||||
- If your approach is materially different, explain the difference and why a second PR is needed in the issue thread.
|
||||
|
||||
### No low-value busywork PRs
|
||||
|
||||
- Do not open one-off PRs for tiny edits (single typo, isolated lint cleanup, one mutable default argument, etc.).
|
||||
- Mechanical cleanups are acceptable only when bundled into a clear, systematic scope — not as isolated first contributions.
|
||||
|
||||
### Human accountability
|
||||
|
||||
- Pure code-agent PRs are not allowed. A human contributor must understand and be able to defend the change.
|
||||
- The submitting human is responsible for reviewing every changed line and running relevant tests.
|
||||
|
||||
## More Details
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for full setup instructions, testing details, and the contribution workflow.
|
||||
+8128
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
cff-version: 1.2.0
|
||||
message: Please cite this project using these metadata.
|
||||
title: "Gradio: Hassle-free sharing and testing of ML models in the wild"
|
||||
abstract: >-
|
||||
Accessibility is a major challenge of machine learning (ML).
|
||||
Typical ML models are built by specialists and require
|
||||
specialized hardware/software as well as ML experience to
|
||||
validate. This makes it challenging for non-technical
|
||||
collaborators and endpoint users (e.g. physicians) to easily
|
||||
provide feedback on model development and to gain trust in
|
||||
ML. The accessibility challenge also makes collaboration
|
||||
more difficult and limits the ML researcher's exposure to
|
||||
realistic data and scenarios that occur in the wild. To
|
||||
improve accessibility and facilitate collaboration, we
|
||||
developed an open-source Python package, Gradio, which
|
||||
allows researchers to rapidly generate a visual interface
|
||||
for their ML models. Gradio makes accessing any ML model as
|
||||
easy as sharing a URL. Our development of Gradio is informed
|
||||
by interviews with a number of machine learning researchers
|
||||
who participate in interdisciplinary collaborations. Their
|
||||
feedback identified that Gradio should support a variety of
|
||||
interfaces and frameworks, allow for easy sharing of the
|
||||
interface, allow for input manipulation and interactive
|
||||
inference by the domain expert, as well as allow embedding
|
||||
the interface in iPython notebooks. We developed these
|
||||
features and carried out a case study to understand Gradio's
|
||||
usefulness and usability in the setting of a machine
|
||||
learning collaboration between a researcher and a
|
||||
cardiologist.
|
||||
authors:
|
||||
- family-names: Abid
|
||||
given-names: Abubakar
|
||||
- family-names: Abdalla
|
||||
given-names: Ali
|
||||
- family-names: Abid
|
||||
given-names: Ali
|
||||
- family-names: Khan
|
||||
given-names: Dawood
|
||||
- family-names: Alfozan
|
||||
given-names: Abdulrahman
|
||||
- family-names: Zou
|
||||
given-names: James
|
||||
doi: 10.48550/arXiv.1906.02569
|
||||
date-released: 2019-06-06
|
||||
url: https://arxiv.org/abs/1906.02569
|
||||
@@ -0,0 +1,134 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official email address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
[Discord], or at our [Email].
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
[Discord]: https://discord.com/invite/feTf9x3ZSB
|
||||
[Email]: gradio-team@huggingface.co
|
||||
+526
@@ -0,0 +1,526 @@
|
||||
# Contributing to Gradio
|
||||
|
||||

|
||||
|
||||
|
||||
More than 300 awesome developers have contributed to the `gradio` library, and the best way to help us today is by **opening a detailed issue** rather than a pull request.
|
||||
|
||||
## 📝 The best way to contribute: open a great issue
|
||||
|
||||
We have **paused accepting pull requests from outside the Gradio maintainer team.** As AI coding agents have become widespread, the volume of community PRs has grown to the point where reviewing them carefully takes more maintainer time than the alternative: pointing our own coding agents at a well-described issue and solving it ourselves.
|
||||
|
||||
So the single most valuable thing you can do is **file a clear, detailed, reproducible issue.** A great issue lets us (and our agents) understand and fix the problem quickly, and it helps us prioritize what to work on next for Gradio.
|
||||
|
||||
> If you are a member of the Gradio maintainer team, the local setup, testing, and PR sections further down still apply to you.
|
||||
|
||||
### 🐛 Reporting a bug
|
||||
|
||||
Please [open an issue](https://github.com/gradio-app/gradio/issues/new/choose) and include:
|
||||
|
||||
1. **A clear, descriptive title** that summarizes the problem.
|
||||
2. **What you expected to happen** vs. **what actually happened.**
|
||||
3. **Step-by-step instructions to reproduce the bug**, numbered and as specific as possible (which component, which event, what input, etc.).
|
||||
4. **A minimal reproduction.** This is the most important part. Please include the smallest self-contained Gradio app that triggers the bug, for example:
|
||||
- a short code snippet (ideally runnable as-is), and/or
|
||||
- a link to a minimal hosted demo on [Hugging Face Spaces](https://huggingface.co/spaces) or a small repo that reproduces the issue.
|
||||
5. **Your environment**: Gradio version (`gradio --version` or `gr.__version__`), Python version, operating system, and browser (for frontend issues).
|
||||
6. **Screenshots, screen recordings, or error tracebacks** where relevant.
|
||||
|
||||
The smaller and more self-contained the reproduction, the faster we can fix it.
|
||||
|
||||
[#10568](https://github.com/gradio-app/gradio/issues/10568) is a good example of a bug report to follow.
|
||||
|
||||
### ✨ Requesting a feature
|
||||
|
||||
Please [open an issue](https://github.com/gradio-app/gradio/issues/new/choose) and include:
|
||||
|
||||
1. **A clear, descriptive title.**
|
||||
2. **The problem or use case** you're trying to solve — what are you trying to build, and where does Gradio fall short today?
|
||||
3. **A detailed description of the feature** you'd like, including a proposed API or example of how you imagine it would be used in code.
|
||||
4. **A demo or mockup** where possible — a link to a small repro/demo, a sketch, or a screenshot of a similar feature elsewhere helps us understand the request quickly.
|
||||
5. **Alternatives you've considered**, including any workarounds you're currently using.
|
||||
|
||||
Detailed feature requests directly shape our roadmap and help us prioritize what to build next.
|
||||
|
||||
[#12519](https://github.com/gradio-app/gradio/issues/12519) is a good example of a feature request issue to follow.
|
||||
|
||||
## 🏡 Setup Gradio locally
|
||||
|
||||
> The following sections (local setup, structure, testing, and submitting PRs) are primarily for the Gradio maintainer team. If you're an outside contributor, the most helpful thing you can do is [open a detailed issue](#-the-best-way-to-contribute-open-a-great-issue) — but you're also welcome to run Gradio locally to build a clean reproduction for your issue.
|
||||
|
||||
There are a few ways to install and run Gradio.
|
||||
|
||||
### 🛠️ Install Gradio from `main`
|
||||
|
||||
- Clone this repo
|
||||
- Navigate to the repo directory and run:
|
||||
<table>
|
||||
<tr>
|
||||
<th>MacOS / Linux</th>
|
||||
<th>Windows</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
```bash
|
||||
bash scripts/install_gradio.sh
|
||||
```
|
||||
</td>
|
||||
<td>
|
||||
|
||||
```bash
|
||||
scripts\install_gradio.bat
|
||||
```
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
- Run the frontend (only required if you are making changes to the frontend and would like to preview them)
|
||||
<table>
|
||||
<tr>
|
||||
<th>MacOS / Linux</th>
|
||||
<th>Windows</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
```bash
|
||||
bash scripts/run_frontend.sh
|
||||
```
|
||||
</td>
|
||||
<td>
|
||||
|
||||
```bash
|
||||
scripts\run_frontend.bat
|
||||
```
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
- Install test requirements (only required if you want to run tests locally)
|
||||
|
||||
(Note that it is highly recommended to use a virtual environment running **Python 3.10** since the versions of Gradio's dependencies are pinned)
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>MacOS / Linux</th>
|
||||
<th>Windows</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
```bash
|
||||
bash scripts/install_test_requirements.sh
|
||||
```
|
||||
</td>
|
||||
<td>
|
||||
|
||||
```bash
|
||||
scripts\install_test_requirements.bat
|
||||
```
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
If you have a different Python version and conflicting packages during the installation, please first run:
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>MacOS / Linux</th>
|
||||
<th>Windows</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
```bash
|
||||
bash scripts/create_test_requirements.sh
|
||||
```
|
||||
</td>
|
||||
<td>
|
||||
|
||||
```bash
|
||||
scripts\create_test_requirements.bat
|
||||
```
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
### 📦 Using dev containers
|
||||
|
||||
Instead of installing Gradio locally, you can alternatively use dev containers. This is supported on all platforms (macOS/Windows/Linux), as well as on GitHub Codespaces.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- An editor which supports dev containers, like VS Code
|
||||
- Docker support on the host computer:
|
||||
- macOS: [Docker Desktop 2.0+](https://www.docker.com/products/docker-desktop/)
|
||||
- Windows: [Docker Desktop 2.0+](https://www.docker.com/products/docker-desktop/)
|
||||
- Linux: [Docker CE/EE 18.06+](https://docs.docker.com/get-docker/) and [Docker Compose 1.21+](https://docs.docker.com/compose/install/)
|
||||
- If using VS Code, the [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension
|
||||
|
||||
Steps:
|
||||
|
||||
- Clone repository
|
||||
- Open it in your editor
|
||||
- For VS Code, execute `Dev Containers: Reopen in container` command
|
||||
|
||||
For detailed instructions, please see the [Dev Containers tutorial](https://code.visualstudio.com/docs/devcontainers/tutorial).
|
||||
|
||||
## 🧱 Structure of the Repository
|
||||
|
||||
If you're a newcomer to Gradio, we recommend getting familiar with the overall structure of the repository so that you can focus on the part of the source code you'd like to contribute to.
|
||||
|
||||
- `/gradio`: contains the Python source code for the library
|
||||
- `/gradio/interface.py`: contains the Python source code for the core `Interface` class
|
||||
- `/gradio/blocks.py`: contains the Python source code for the core `Blocks` class
|
||||
- `/gradio/components/`: the directory that contains the Python source code for all of the Gradio components.
|
||||
- `/test`: contains Python unit tests for the library
|
||||
- `/js`: contains the HTML/JS/CSS source code for the library, including the fronted code for each component in a separate directory
|
||||
- `/js/_website`: contains the code for the Gradio website (www.gradio.app). See the README in the `/js/_website` folder for more details
|
||||
- `/guides`: the written guides and tutorials that are found on Gradio's website.
|
||||
|
||||
## 🚀 Run a Gradio app
|
||||
|
||||
You can get started by creating an `app.py` file in the root:
|
||||
|
||||
```py
|
||||
import gradio as gr
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
gr.Button()
|
||||
|
||||
if __name__ == "__main__":
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
then run:
|
||||
|
||||
```
|
||||
gradio app.py
|
||||
```
|
||||
|
||||
This will start the backend server in reload mode, which will watch for changes in the `gradio` folder and reload the app if changes are made. By default, Gradio will launch on port 7860. You can also just use `python app.py`, but this won't automatically trigger updates.
|
||||
|
||||
Note: if you have `gradio` installed elsewhere in your system, you may need to uninstall it or at least make sure your `PYTHONPATH` includes the directory where the Gradio repository is cloned, e.g.,
|
||||
`export PYTHONPATH="./"`
|
||||
|
||||
|
||||
If you're making frontend changes, start the frontend server:
|
||||
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>MacOS / Linux</th>
|
||||
<th>Windows</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
```bash
|
||||
bash scripts/run_frontend.sh
|
||||
```
|
||||
</td>
|
||||
<td>
|
||||
|
||||
```bash
|
||||
scripts\run_frontend.bat
|
||||
```
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
This will open a separate browser tab. By default, Gradio will launch this on port 9876. Any changes to the frontend will also reload automatically in the browser. For more information about developing in the frontend, you can refer to [js/README.md](js/README.md).
|
||||
|
||||
We also have demos of all our components in the `/gradio/demo` directory. To get our simple gradio Chatbot running locally:
|
||||
|
||||
```
|
||||
gradio demo/chatbot_simple/run.py
|
||||
```
|
||||
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
We use Pytest, Playwright and Vitest to test our code.
|
||||
|
||||
- The Python tests are located in `/test`. To run these tests:
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>MacOS / Linux</th>
|
||||
<th>Windows</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
```
|
||||
bash scripts/run_backend_tests.sh
|
||||
```
|
||||
</td>
|
||||
<td>
|
||||
|
||||
```bash
|
||||
scripts\run_backend_tests.bat
|
||||
```
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
- The frontend unit tests are any defined with the filename `*.test.ts`. To run them:
|
||||
|
||||
```
|
||||
pnpm test
|
||||
```
|
||||
|
||||
- Browser tests are located in `js/spa/test` and are defined as `*spec.ts` files.
|
||||
|
||||
To install browser test dependencies:
|
||||
|
||||
```
|
||||
pnpm exec playwright install chromium firefox
|
||||
pnpm exec playwright install-deps chromium firefox
|
||||
pnpm --filter @gradio/utils --filter @gradio/theme package
|
||||
```
|
||||
|
||||
To run browser tests:
|
||||
|
||||
```
|
||||
pnpm test:browser
|
||||
```
|
||||
|
||||
To build the frontend code before running browser tests:
|
||||
|
||||
```
|
||||
pnpm test:browser:full
|
||||
```
|
||||
|
||||
You can also run browser tests in the UI mode by adding the `--ui` flag:
|
||||
|
||||
```
|
||||
pnpm test:browser --ui
|
||||
```
|
||||
|
||||
If you have made any significant visual changes to a component, we encourage you to add a new Storybook story or amend an existing one to reflect them. You can create a new story with a `*.stories.svelte` file. You can run the storybook locally:
|
||||
|
||||
```
|
||||
pnpm storybook
|
||||
```
|
||||
|
||||
## ✍️ Gradio Website & Docs
|
||||
|
||||
We also welcome any contributions to our [website](https://www.gradio.app) and [docs](https://www.gradio.app/docs).
|
||||
|
||||
### Building The Website
|
||||
|
||||
All of the website code lives in the `js/_website/` directory.
|
||||
|
||||
To start the website on dev mode simply cd into this directory and run:
|
||||
|
||||
```
|
||||
pnpm i
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
This will serve the website on `http://localhost:5173/` (or the next available port).
|
||||
|
||||
When you're done with changes and want to build the website you can run:
|
||||
|
||||
```
|
||||
pnpm build && pnpm preview
|
||||
```
|
||||
|
||||
This will serve the website on `http://localhost:4173/` (or the next available port).
|
||||
|
||||
### Documentation
|
||||
#### API Reference
|
||||
|
||||
Gradio's [API reference](https://www.gradio.app/docs/gradio/interface) is built from templates written in [mdsvex](https://mdsvex.pngwn.io/). You can find all the templates in this directory:
|
||||
|
||||
```
|
||||
js/_website/src/lib/templates/gradio
|
||||
```
|
||||
|
||||
The templates directory is structured as follows:
|
||||
|
||||
```
|
||||
├── gradio/
|
||||
│ ├── 01_building-demos/
|
||||
│ │ ├── 01_interface.svx
|
||||
│ │ ├── 02_chatinterface.svx
|
||||
│ │ ├── 03_tabbedinterface.svx
|
||||
│ │ ├── 04_blocks.svx
|
||||
│ ├── 02_blocks-layout/
|
||||
│ ├── 03_components/
|
||||
│ ├── 04_helpers/
|
||||
│ ├── 05_modals/
|
||||
│ ├── 06_routes/
|
||||
│ ├── other/
|
||||
```
|
||||
|
||||
This structure defines the pages' ordering. You can use a numeral prefix (XX_) before a name to dictate where a page is listed, but it's otherwise ignored in the url route. Note that the folder names (01_building-demos, etc) are only used for the navbar and are not in the url.
|
||||
|
||||
The mdsvex files use a combination of markdown and svelte. They also pull documentation directly from the source code. Adding a `@document()` wrapper around any class or function in the source code will make its docstrings available in the templates.
|
||||
|
||||
Here's an example: the template for [Image docs](https://www.gradio.app/docs/gradio/image) is [here](https://github.com/gradio-app/gradio/blob/main/js/_website/src/lib/templates/gradio/03_components/image.svx). You can see the initialization section references `obj.parameters`. So to edit the description of a parameter you'll have to edit the docstring in the [source code](https://github.com/gradio-app/gradio/blob/main/gradio/components/image.py). But the page also includes a section titled 'GIF and SVG Image Formats' which is written in plain markdown and can be edited directly on the template.
|
||||
|
||||
If you are making changes to docstrings and want to see them on the website you have to make sure you're on an editable install of the gradio library. Just run this command from root:
|
||||
|
||||
```
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
And then from the website directory:
|
||||
|
||||
```
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
#### Guides
|
||||
|
||||
Guides like [Quickstart](https://www.gradio.app/guides/quickstart) are built from this directory: `/guides`. The directory follows the same structure as the API reference templates, with nested folders and numerical prefixes for ordering, but the files are standard markdown files. After adding a new guide, or editing an existing one, to see the changes on the website make sure you are on an editable install of the gradio library. Run this command from root:
|
||||
|
||||
```
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
and then from the website directory:
|
||||
|
||||
```
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
#### Main vs. Released
|
||||
|
||||
The website supports documentation for both the latest released version on pypi as well as the main build on github. You can switch between them on the website by using the toggle on any page or by prefixing '/main' before the route in the url. For example: https://www.gradio.app/main/guides/quickstart
|
||||
|
||||
If you're making changes to documentation and are wondering why they're not showing up, make sure you're looking at the 'main' version of the page. Since they haven't been included in a release yet, they will only be visible there.
|
||||
|
||||
## 📮 Submitting PRs
|
||||
|
||||
All PRs should be submitted against `main`, and ideally should address an open issue, unless the change is small. Direct commits to main are blocked, and PRs require an approving review to merge into main. By convention, the Gradio maintainers will review PRs when:
|
||||
|
||||
- An initial review has been requested
|
||||
- A clear, descriptive title has been assigned to the PR
|
||||
- A maintainer (@abidlabs, @aliabid94, @aliabd, @AK391, @dawoodkhan82, @pngwn, @freddyaboulton, @hannahblair, @hysts, @whitphx) is tagged in the PR comments and asked to complete a review
|
||||
|
||||
🧹 We ask that you make sure initial CI checks are passing before requesting a review. One of the Gradio maintainers will merge the PR when all the checks are passing. You can safely ignore the Vercel and Spaces checks, which only run under maintainers' pull requests.
|
||||
|
||||
Don't forget to format your code before pushing:
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>MacOS / Linux</th>
|
||||
<th>Windows</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
```
|
||||
bash scripts/format_backend.sh
|
||||
```
|
||||
</td>
|
||||
<td>
|
||||
|
||||
```bash
|
||||
scripts\format_backend.bat
|
||||
```
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
And if you made changes to the frontend:
|
||||
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>MacOS / Linux</th>
|
||||
<th>Windows</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
```
|
||||
bash scripts/format_frontend.sh
|
||||
```
|
||||
</td>
|
||||
<td>
|
||||
|
||||
```bash
|
||||
scripts\format_frontend.bat
|
||||
```
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
Thank you for taking the time to contribute to Gradio!
|
||||
|
||||
|
||||
## ❓ Need help or have questions?
|
||||
|
||||
- [Open a detailed issue](https://github.com/gradio-app/gradio/issues/new/choose) for a bug or feature request — see the [guidelines above](#-the-best-way-to-contribute-open-a-great-issue).
|
||||
- Ask the Gradio community in our [Discord](https://discord.com/invite/feTf9x3ZSB).
|
||||
- Browse existing [issues](https://github.com/gradio-app/gradio/issues) to see if your problem has already been reported (and add a 👍 or extra reproduction details if so).
|
||||
|
||||
## 🚧 Troubleshooting
|
||||
`ERROR: Error loading ASGI app. Could not import module "<filename>"`
|
||||
|
||||
Verify that you've used the correct filename of your gradio app, and that you're in the directory of the file.
|
||||
|
||||
---
|
||||
|
||||
```ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL @self/spa@1.0.0 build:local: vite build --mode production:local --emptyOutDir "--emptyOutDir"```
|
||||
|
||||
Delete `/node_modules` and `pnpm-lock.yaml`:
|
||||
|
||||
```
|
||||
rm -rf node_modules/
|
||||
rm pnpm-lock.yaml
|
||||
```
|
||||
|
||||
and run the install scripts:
|
||||
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>MacOS / Linux</th>
|
||||
<th>Windows</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
```
|
||||
bash scripts/install_gradio.sh
|
||||
bash scripts/build_frontend.sh
|
||||
```
|
||||
</td>
|
||||
<td>
|
||||
|
||||
```bash
|
||||
scripts\install_gradio.bat
|
||||
scripts\build_frontend.bat
|
||||
```
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
---
|
||||
|
||||
```FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory``` when running `scripts/build_frontend.sh`.
|
||||
|
||||
Run `scripts/build_frontend.sh` with the environment variable `NODE_OPTIONS=--max_old_space_size=8192` to increase the heap size.
|
||||
|
||||
---
|
||||
|
||||
In the case of:
|
||||
- Unexpected exceptions being thrown, or
|
||||
- The following warning:
|
||||
`IMPORTANT: You are using gradio version <earlier version>, however version <later version> is available, please upgrade.`
|
||||
|
||||
ensure your `PYTHONPATH` includes the directory where the Gradio repository is cloned, e.g.:
|
||||
|
||||
```export PYTHONPATH="./"```
|
||||
|
||||
This ensures that when `gradio` is imported in a python program, it is this current version from this repository.
|
||||
|
||||
---
|
||||
|
||||
_Could these guidelines be clearer? Feel free to open a PR to help us facilitate open-source contributions!_
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,227 @@
|
||||
<!-- DO NOT EDIT THIS FILE DIRECTLY. INSTEAD EDIT THE `readme_template.md` OR `guides/01_getting-started/01_quickstart.md` TEMPLATES AND THEN RUN `render_readme.py` SCRIPT. -->
|
||||
|
||||
<div align="center">
|
||||
<a href="https://gradio.app">
|
||||
<img src="readme_files/gradio.svg" alt="gradio" width=350>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<span>
|
||||
<a href="https://www.producthunt.com/posts/gradio-5-0?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-gradio-5-0" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=501906&theme=light" alt="Gradio 5.0 - the easiest way to build AI web apps | Product Hunt" style="width: 150px; height: 54px;" width="150" height="54" /></a>
|
||||
<a href="https://trendshift.io/repositories/2145" target="_blank"><img src="https://trendshift.io/api/badge/repositories/2145" alt="gradio-app%2Fgradio | Trendshift" style="width: 150px; height: 55px;" width="150" height="55"/></a>
|
||||
</span>
|
||||
|
||||
[](https://github.com/gradio-app/gradio/actions/workflows/test-python.yml)
|
||||
[](https://github.com/gradio-app/gradio/actions/workflows/tests-js.yml)
|
||||
[](https://pypi.org/project/gradio/)
|
||||
[](https://pypi.org/project/gradio/)
|
||||

|
||||
[](https://twitter.com/gradio)
|
||||
|
||||
[Website](https://gradio.app)
|
||||
| [Documentation](https://gradio.app/docs/)
|
||||
| [Guides](https://gradio.app/guides/)
|
||||
| [Getting Started](https://gradio.app/getting_started/)
|
||||
| [Examples](demo/)
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
English | [中文](readme_files/zh-cn#readme)
|
||||
|
||||
</div>
|
||||
|
||||
# Gradio: Build Machine Learning Web Apps — in Python
|
||||
|
||||
|
||||
|
||||
Gradio is an open-source Python package that allows you to quickly **build** a demo or web application for your machine learning model, API, or any arbitrary Python function. You can then **share** a link to your demo or web application in just a few seconds using Gradio's built-in sharing features. *No JavaScript, CSS, or web hosting experience needed!*
|
||||
|
||||
<img src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/gif-version.gif" style="padding-bottom: 10px">
|
||||
|
||||
It just takes a few lines of Python to create your own demo, so let's get started 💫
|
||||
|
||||
|
||||
### Installation
|
||||
|
||||
**Prerequisite**: Gradio requires [Python 3.10 or higher](https://www.python.org/downloads/).
|
||||
|
||||
|
||||
We recommend installing Gradio using `pip`, which is included by default in Python. Run this in your terminal or command prompt:
|
||||
|
||||
```bash
|
||||
pip install --upgrade gradio
|
||||
```
|
||||
|
||||
|
||||
> [!TIP]
|
||||
> It is best to install Gradio in a virtual environment. Detailed installation instructions for all common operating systems <a href="https://www.gradio.app/main/guides/installing-gradio-in-a-virtual-environment">are provided here</a>.
|
||||
|
||||
### Building Your First Demo
|
||||
|
||||
You can run Gradio in your favorite code editor, Jupyter notebook, Google Colab, or anywhere else you write Python. Let's write your first Gradio app:
|
||||
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
|
||||
def greet(name, intensity):
|
||||
return "Hello, " + name + "!" * int(intensity)
|
||||
|
||||
demo = gr.Interface(
|
||||
fn=greet,
|
||||
inputs=["text", "slider"],
|
||||
outputs=["text"],
|
||||
api_name="predict"
|
||||
)
|
||||
|
||||
demo.launch()
|
||||
```
|
||||
|
||||
|
||||
|
||||
> [!TIP]
|
||||
> We shorten the imported name from <code>gradio</code> to <code>gr</code>. This is a widely adopted convention for better readability of code.
|
||||
|
||||
Now, run your code. If you've written the Python code in a file named `app.py`, then you would run `python app.py` from the terminal.
|
||||
|
||||
The demo below will open in a browser on [http://localhost:7860](http://localhost:7860) if running from a file. If you are running within a notebook, the demo will appear embedded within the notebook.
|
||||
|
||||

|
||||
|
||||
Type your name in the textbox on the left, drag the slider, and then press the Submit button. You should see a friendly greeting on the right.
|
||||
|
||||
> [!TIP]
|
||||
> When developing locally, you can run your Gradio app in <strong>hot reload mode</strong>, which automatically reloads the Gradio app whenever you make changes to the file. To do this, simply type in <code>gradio</code> before the name of the file instead of <code>python</code>. In the example above, you would type: `gradio app.py` in your terminal. You can also enable <strong>vibe mode</strong> by using the <code>--vibe</code> flag, e.g. <code>gradio --vibe app.py</code>, which provides an in-browser chat that can be used to write or edit your Gradio app using natural language. Learn more in the <a href="https://www.gradio.app/guides/developing-faster-with-reload-mode">Hot Reloading Guide</a>.
|
||||
|
||||
|
||||
**Understanding the `Interface` Class**
|
||||
|
||||
You'll notice that in order to make your first demo, you created an instance of the `gr.Interface` class. The `Interface` class is designed to create demos for machine learning models which accept one or more inputs, and return one or more outputs.
|
||||
|
||||
The `Interface` class has three core arguments:
|
||||
|
||||
- `fn`: the function to wrap a user interface (UI) around
|
||||
- `inputs`: the Gradio component(s) to use for the input. The number of components should match the number of arguments in your function.
|
||||
- `outputs`: the Gradio component(s) to use for the output. The number of components should match the number of return values from your function.
|
||||
|
||||
The `fn` argument is very flexible -- you can pass *any* Python function that you want to wrap with a UI. In the example above, we saw a relatively simple function, but the function could be anything from a music generator to a tax calculator to the prediction function of a pretrained machine learning model.
|
||||
|
||||
The `inputs` and `outputs` arguments take one or more Gradio components. As we'll see, Gradio includes more than [30 built-in components](https://www.gradio.app/docs/gradio/introduction) (such as the `gr.Textbox()`, `gr.Image()`, and `gr.HTML()` components) that are designed for machine learning applications.
|
||||
|
||||
> [!TIP]
|
||||
> For the `inputs` and `outputs` arguments, you can pass in the name of these components as a string (`"textbox"`) or an instance of the class (`gr.Textbox()`).
|
||||
|
||||
If your function accepts more than one argument, as is the case above, pass a list of input components to `inputs`, with each input component corresponding to one of the arguments of the function, in order. The same holds true if your function returns more than one value: simply pass in a list of components to `outputs`. This flexibility makes the `Interface` class a very powerful way to create demos.
|
||||
|
||||
We'll dive deeper into the `gr.Interface` on our series on [building Interfaces](https://www.gradio.app/main/guides/the-interface-class).
|
||||
|
||||
### Sharing Your Demo
|
||||
|
||||
What good is a beautiful demo if you can't share it? Gradio lets you easily share a machine learning demo without having to worry about the hassle of hosting on a web server. Simply set `share=True` in `launch()`, and a publicly accessible URL will be created for your demo. Let's revisit our example demo, but change the last line as follows:
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
|
||||
def greet(name):
|
||||
return "Hello " + name + "!"
|
||||
|
||||
demo = gr.Interface(fn=greet, inputs="textbox", outputs="textbox")
|
||||
|
||||
demo.launch(share=True) # Share your demo with just 1 extra parameter 🚀
|
||||
```
|
||||
|
||||
When you run this code, a public URL will be generated for your demo in a matter of seconds, something like:
|
||||
|
||||
👉 `https://a23dsf231adb.gradio.live`
|
||||
|
||||
Now, anyone around the world can try your Gradio demo from their browser, while the machine learning model and all computation continues to run locally on your computer.
|
||||
|
||||
To learn more about sharing your demo, read our dedicated guide on [sharing your Gradio application](https://www.gradio.app/guides/sharing-your-app).
|
||||
|
||||
|
||||
### An Overview of Gradio
|
||||
|
||||
So far, we've been discussing the `Interface` class, which is a high-level class that lets you build demos quickly with Gradio. But what else does Gradio include?
|
||||
|
||||
#### Custom Demos with `gr.Blocks`
|
||||
|
||||
Gradio offers a low-level approach for designing web apps with more customizable layouts and data flows with the `gr.Blocks` class. Blocks supports things like controlling where components appear on the page, handling multiple data flows and more complex interactions (e.g. outputs can serve as inputs to other functions), and updating properties/visibility of components based on user interaction — still all in Python.
|
||||
|
||||
You can build very custom and complex applications using `gr.Blocks()`. For example, the popular image generation [Automatic1111 Web UI](https://github.com/AUTOMATIC1111/stable-diffusion-webui) is built using Gradio Blocks. We dive deeper into the `gr.Blocks` on our series on [building with Blocks](https://www.gradio.app/guides/blocks-and-event-listeners).
|
||||
|
||||
#### Chatbots with `gr.ChatInterface`
|
||||
|
||||
Gradio includes another high-level class, `gr.ChatInterface`, which is specifically designed to create Chatbot UIs. Similar to `Interface`, you supply a function and Gradio creates a fully working Chatbot UI. If you're interested in creating a chatbot, you can jump straight to [our dedicated guide on `gr.ChatInterface`](https://www.gradio.app/guides/creating-a-chatbot-fast).
|
||||
|
||||
#### The Gradio Python & JavaScript Ecosystem
|
||||
|
||||
That's the gist of the core `gradio` Python library, but Gradio is actually so much more! It's an entire ecosystem of Python and JavaScript libraries that let you build machine learning applications, or query them programmatically, in Python or JavaScript. Here are other related parts of the Gradio ecosystem:
|
||||
|
||||
* [Gradio Python Client](https://www.gradio.app/guides/getting-started-with-the-python-client) (`gradio_client`): query any Gradio app programmatically in Python.
|
||||
* [Gradio JavaScript Client](https://www.gradio.app/guides/getting-started-with-the-js-client) (`@gradio/client`): query any Gradio app programmatically in JavaScript.
|
||||
* [Hugging Face Spaces](https://huggingface.co/spaces): the most popular place to host Gradio applications — for free!
|
||||
* [Server mode](https://www.gradio.app/guides/server-mode) (`gradio.Server`): build a custom frontend with Gradio's backend — queue, streaming, MCP, ZeroGPU, and Spaces hosting included.
|
||||
|
||||
### What's Next?
|
||||
|
||||
Keep learning about Gradio sequentially using the Gradio Guides, which include explanations as well as example code and embedded interactive demos. Next up: [let's dive deeper into the Interface class](https://www.gradio.app/guides/the-interface-class).
|
||||
|
||||
Or, if you already know the basics and are looking for something specific, you can search the more [technical API documentation](https://www.gradio.app/docs/).
|
||||
|
||||
|
||||
### AI Coding Skills
|
||||
|
||||
Gradio provides a "skill" that enriches AI coding assistants (like Cursor, Claude Code, Codex, etc.) with Gradio-specific knowledge, so that they can build Gradio apps more effectively. This is especially useful when creating custom Gradio components or styling. Install the Gradio skill for your coding assistant with a single command:
|
||||
|
||||
```bash
|
||||
gradio skills add --cursor # or --claude, --codex, --opencode
|
||||
```
|
||||
|
||||
Use `--global` to install at the user level (applies to all projects). Your skill will be automatically available for the particular coding agent.
|
||||
|
||||
You can also install a skill for a **specific Gradio Space**, which generates API usage docs (Python, JS, cURL) on the fly:
|
||||
|
||||
```bash
|
||||
gradio skills add abidlabs/en2fr --cursor
|
||||
```
|
||||
|
||||
## Questions?
|
||||
|
||||
If you'd like to report a bug or have a feature request, please create an [issue on GitHub](https://github.com/gradio-app/gradio/issues/new/choose). For general questions about usage, we are available on [our Discord server](https://discord.com/invite/feTf9x3ZSB) and happy to help.
|
||||
|
||||
If you like Gradio, please leave us a ⭐ on GitHub!
|
||||
|
||||
## Open Source Stack
|
||||
|
||||
Gradio is built on top of many wonderful open-source libraries!
|
||||
|
||||
[<img src="readme_files/huggingface_mini.svg" alt="huggingface" height=40>](https://huggingface.co)
|
||||
[<img src="readme_files/python.svg" alt="python" height=40>](https://www.python.org)
|
||||
[<img src="readme_files/fastapi.svg" alt="fastapi" height=40>](https://fastapi.tiangolo.com)
|
||||
[<img src="readme_files/encode.svg" alt="encode" height=40>](https://www.encode.io)
|
||||
[<img src="readme_files/svelte.svg" alt="svelte" height=40>](https://svelte.dev)
|
||||
[<img src="readme_files/vite.svg" alt="vite" height=40>](https://vitejs.dev)
|
||||
[<img src="readme_files/pnpm.svg" alt="pnpm" height=40>](https://pnpm.io)
|
||||
[<img src="readme_files/tailwind.svg" alt="tailwind" height=40>](https://tailwindcss.com)
|
||||
[<img src="readme_files/storybook.svg" alt="storybook" height=40>](https://storybook.js.org/)
|
||||
[<img src="readme_files/chromatic.svg" alt="chromatic" height=40>](https://www.chromatic.com/)
|
||||
|
||||
## License
|
||||
|
||||
Gradio is licensed under the Apache License 2.0 found in the [LICENSE](LICENSE) file in the root directory of this repository.
|
||||
|
||||
## Citation
|
||||
|
||||
Also check out the paper _[Gradio: Hassle-Free Sharing and Testing of ML Models in the Wild](https://arxiv.org/abs/1906.02569), ICML HILL 2019_, and please cite it if you use Gradio in your work.
|
||||
|
||||
```
|
||||
@article{abid2019gradio,
|
||||
title = {Gradio: Hassle-Free Sharing and Testing of ML Models in the Wild},
|
||||
author = {Abid, Abubakar and Abdalla, Ali and Abid, Ali and Khan, Dawood and Alfozan, Abdulrahman and Zou, James},
|
||||
journal = {arXiv preprint arXiv:1906.02569},
|
||||
year = {2019},
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- 原始项目:`gradio-app/gradio`
|
||||
- 原始仓库:https://github.com/gradio-app/gradio
|
||||
- 导入方式:上游默认分支的最新快照
|
||||
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||
@@ -0,0 +1,5 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability, we would be very grateful if you could email us at gradio-team@huggingface.co. This is the preferred approach instead of opening a public issue. We take all vulnerability reports seriously, and will work to patch the vulnerability immediately. Whenever possible, we will credit the person or people who report the security vulnerabilities after it has been patched.
|
||||
+517
@@ -0,0 +1,517 @@
|
||||
------formdata-undici-075560669371
|
||||
Content-Disposition: form-data; name="metadata"
|
||||
|
||||
{"main_module":"functionsWorker-0.6571259265887377.js"}
|
||||
------formdata-undici-075560669371
|
||||
Content-Disposition: form-data; name="functionsWorker-0.6571259265887377.js"; filename="functionsWorker-0.6571259265887377.js"
|
||||
Content-Type: application/javascript+module
|
||||
|
||||
// _shared.ts
|
||||
var AI_UA_PATTERNS = [
|
||||
/\bgptbot\b/i,
|
||||
/\bchatgpt-user\b/i,
|
||||
/\bclaudebot\b/i,
|
||||
/\bclaude-web\b/i,
|
||||
/\bclaude-user\b/i,
|
||||
/\banthropic\b/i,
|
||||
/\bperplexitybot\b/i,
|
||||
/\bmeta-external(fetcher|agent)\b/i,
|
||||
/\bfacebookbot\b/i,
|
||||
/\bamazonbot\b/i,
|
||||
/\bapplebot\b/i,
|
||||
/\bbytespider\b/i,
|
||||
/\bccbot\b/i,
|
||||
/\bcohere\b/i,
|
||||
/\bgoogle-extended\b/i
|
||||
];
|
||||
function isLLMRequest(request) {
|
||||
const ua = request.headers.get("user-agent") || "";
|
||||
const accept = request.headers.get("accept") || "";
|
||||
if (accept.includes("text/markdown"))
|
||||
return true;
|
||||
return AI_UA_PATTERNS.some((re) => re.test(ua));
|
||||
}
|
||||
async function serveDocMarkdown(context) {
|
||||
const { request, params, next } = context;
|
||||
if (!isLLMRequest(request))
|
||||
return next();
|
||||
const url = new URL(request.url);
|
||||
return Response.redirect(`${url.origin}/api/markdown/${params.doc}`, 302);
|
||||
}
|
||||
async function serveGuideMarkdown(context) {
|
||||
const { request, params, next } = context;
|
||||
if (!isLLMRequest(request))
|
||||
return next();
|
||||
const url = new URL(request.url);
|
||||
return Response.redirect(
|
||||
`${url.origin}/api/markdown/guide/${params.guide}`,
|
||||
302
|
||||
);
|
||||
}
|
||||
|
||||
// main/docs/gradio/[doc].ts
|
||||
var onRequestGet = serveDocMarkdown;
|
||||
|
||||
// docs/gradio/[doc].ts
|
||||
var onRequestGet2 = serveDocMarkdown;
|
||||
|
||||
// main/guides/[guide].ts
|
||||
var onRequestGet3 = serveGuideMarkdown;
|
||||
|
||||
// guides/[guide].ts
|
||||
var onRequestGet4 = serveGuideMarkdown;
|
||||
|
||||
// ../../../.wrangler/tmp/pages-CpiPgT/functionsRoutes-0.19632173451245094.mjs
|
||||
var routes = [
|
||||
{
|
||||
routePath: "/main/docs/gradio/:doc",
|
||||
mountPath: "/main/docs/gradio",
|
||||
method: "GET",
|
||||
middlewares: [],
|
||||
modules: [onRequestGet]
|
||||
},
|
||||
{
|
||||
routePath: "/docs/gradio/:doc",
|
||||
mountPath: "/docs/gradio",
|
||||
method: "GET",
|
||||
middlewares: [],
|
||||
modules: [onRequestGet2]
|
||||
},
|
||||
{
|
||||
routePath: "/main/guides/:guide",
|
||||
mountPath: "/main/guides",
|
||||
method: "GET",
|
||||
middlewares: [],
|
||||
modules: [onRequestGet3]
|
||||
},
|
||||
{
|
||||
routePath: "/guides/:guide",
|
||||
mountPath: "/guides",
|
||||
method: "GET",
|
||||
middlewares: [],
|
||||
modules: [onRequestGet4]
|
||||
}
|
||||
];
|
||||
|
||||
// ../../../../../../../Library/pnpm/global/5/.pnpm/path-to-regexp@6.2.2/node_modules/path-to-regexp/dist.es2015/index.js
|
||||
function lexer(str) {
|
||||
var tokens = [];
|
||||
var i = 0;
|
||||
while (i < str.length) {
|
||||
var char = str[i];
|
||||
if (char === "*" || char === "+" || char === "?") {
|
||||
tokens.push({ type: "MODIFIER", index: i, value: str[i++] });
|
||||
continue;
|
||||
}
|
||||
if (char === "\\") {
|
||||
tokens.push({ type: "ESCAPED_CHAR", index: i++, value: str[i++] });
|
||||
continue;
|
||||
}
|
||||
if (char === "{") {
|
||||
tokens.push({ type: "OPEN", index: i, value: str[i++] });
|
||||
continue;
|
||||
}
|
||||
if (char === "}") {
|
||||
tokens.push({ type: "CLOSE", index: i, value: str[i++] });
|
||||
continue;
|
||||
}
|
||||
if (char === ":") {
|
||||
var name = "";
|
||||
var j = i + 1;
|
||||
while (j < str.length) {
|
||||
var code = str.charCodeAt(j);
|
||||
if (
|
||||
// `0-9`
|
||||
code >= 48 && code <= 57 || // `A-Z`
|
||||
code >= 65 && code <= 90 || // `a-z`
|
||||
code >= 97 && code <= 122 || // `_`
|
||||
code === 95
|
||||
) {
|
||||
name += str[j++];
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!name)
|
||||
throw new TypeError("Missing parameter name at ".concat(i));
|
||||
tokens.push({ type: "NAME", index: i, value: name });
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (char === "(") {
|
||||
var count = 1;
|
||||
var pattern = "";
|
||||
var j = i + 1;
|
||||
if (str[j] === "?") {
|
||||
throw new TypeError('Pattern cannot start with "?" at '.concat(j));
|
||||
}
|
||||
while (j < str.length) {
|
||||
if (str[j] === "\\") {
|
||||
pattern += str[j++] + str[j++];
|
||||
continue;
|
||||
}
|
||||
if (str[j] === ")") {
|
||||
count--;
|
||||
if (count === 0) {
|
||||
j++;
|
||||
break;
|
||||
}
|
||||
} else if (str[j] === "(") {
|
||||
count++;
|
||||
if (str[j + 1] !== "?") {
|
||||
throw new TypeError("Capturing groups are not allowed at ".concat(j));
|
||||
}
|
||||
}
|
||||
pattern += str[j++];
|
||||
}
|
||||
if (count)
|
||||
throw new TypeError("Unbalanced pattern at ".concat(i));
|
||||
if (!pattern)
|
||||
throw new TypeError("Missing pattern at ".concat(i));
|
||||
tokens.push({ type: "PATTERN", index: i, value: pattern });
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
tokens.push({ type: "CHAR", index: i, value: str[i++] });
|
||||
}
|
||||
tokens.push({ type: "END", index: i, value: "" });
|
||||
return tokens;
|
||||
}
|
||||
function parse(str, options) {
|
||||
if (options === void 0) {
|
||||
options = {};
|
||||
}
|
||||
var tokens = lexer(str);
|
||||
var _a = options.prefixes, prefixes = _a === void 0 ? "./" : _a;
|
||||
var defaultPattern = "[^".concat(escapeString(options.delimiter || "/#?"), "]+?");
|
||||
var result = [];
|
||||
var key = 0;
|
||||
var i = 0;
|
||||
var path = "";
|
||||
var tryConsume = function(type) {
|
||||
if (i < tokens.length && tokens[i].type === type)
|
||||
return tokens[i++].value;
|
||||
};
|
||||
var mustConsume = function(type) {
|
||||
var value2 = tryConsume(type);
|
||||
if (value2 !== void 0)
|
||||
return value2;
|
||||
var _a2 = tokens[i], nextType = _a2.type, index = _a2.index;
|
||||
throw new TypeError("Unexpected ".concat(nextType, " at ").concat(index, ", expected ").concat(type));
|
||||
};
|
||||
var consumeText = function() {
|
||||
var result2 = "";
|
||||
var value2;
|
||||
while (value2 = tryConsume("CHAR") || tryConsume("ESCAPED_CHAR")) {
|
||||
result2 += value2;
|
||||
}
|
||||
return result2;
|
||||
};
|
||||
while (i < tokens.length) {
|
||||
var char = tryConsume("CHAR");
|
||||
var name = tryConsume("NAME");
|
||||
var pattern = tryConsume("PATTERN");
|
||||
if (name || pattern) {
|
||||
var prefix = char || "";
|
||||
if (prefixes.indexOf(prefix) === -1) {
|
||||
path += prefix;
|
||||
prefix = "";
|
||||
}
|
||||
if (path) {
|
||||
result.push(path);
|
||||
path = "";
|
||||
}
|
||||
result.push({
|
||||
name: name || key++,
|
||||
prefix,
|
||||
suffix: "",
|
||||
pattern: pattern || defaultPattern,
|
||||
modifier: tryConsume("MODIFIER") || ""
|
||||
});
|
||||
continue;
|
||||
}
|
||||
var value = char || tryConsume("ESCAPED_CHAR");
|
||||
if (value) {
|
||||
path += value;
|
||||
continue;
|
||||
}
|
||||
if (path) {
|
||||
result.push(path);
|
||||
path = "";
|
||||
}
|
||||
var open = tryConsume("OPEN");
|
||||
if (open) {
|
||||
var prefix = consumeText();
|
||||
var name_1 = tryConsume("NAME") || "";
|
||||
var pattern_1 = tryConsume("PATTERN") || "";
|
||||
var suffix = consumeText();
|
||||
mustConsume("CLOSE");
|
||||
result.push({
|
||||
name: name_1 || (pattern_1 ? key++ : ""),
|
||||
pattern: name_1 && !pattern_1 ? defaultPattern : pattern_1,
|
||||
prefix,
|
||||
suffix,
|
||||
modifier: tryConsume("MODIFIER") || ""
|
||||
});
|
||||
continue;
|
||||
}
|
||||
mustConsume("END");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function match(str, options) {
|
||||
var keys = [];
|
||||
var re = pathToRegexp(str, keys, options);
|
||||
return regexpToFunction(re, keys, options);
|
||||
}
|
||||
function regexpToFunction(re, keys, options) {
|
||||
if (options === void 0) {
|
||||
options = {};
|
||||
}
|
||||
var _a = options.decode, decode = _a === void 0 ? function(x) {
|
||||
return x;
|
||||
} : _a;
|
||||
return function(pathname) {
|
||||
var m = re.exec(pathname);
|
||||
if (!m)
|
||||
return false;
|
||||
var path = m[0], index = m.index;
|
||||
var params = /* @__PURE__ */ Object.create(null);
|
||||
var _loop_1 = function(i2) {
|
||||
if (m[i2] === void 0)
|
||||
return "continue";
|
||||
var key = keys[i2 - 1];
|
||||
if (key.modifier === "*" || key.modifier === "+") {
|
||||
params[key.name] = m[i2].split(key.prefix + key.suffix).map(function(value) {
|
||||
return decode(value, key);
|
||||
});
|
||||
} else {
|
||||
params[key.name] = decode(m[i2], key);
|
||||
}
|
||||
};
|
||||
for (var i = 1; i < m.length; i++) {
|
||||
_loop_1(i);
|
||||
}
|
||||
return { path, index, params };
|
||||
};
|
||||
}
|
||||
function escapeString(str) {
|
||||
return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1");
|
||||
}
|
||||
function flags(options) {
|
||||
return options && options.sensitive ? "" : "i";
|
||||
}
|
||||
function regexpToRegexp(path, keys) {
|
||||
if (!keys)
|
||||
return path;
|
||||
var groupsRegex = /\((?:\?<(.*?)>)?(?!\?)/g;
|
||||
var index = 0;
|
||||
var execResult = groupsRegex.exec(path.source);
|
||||
while (execResult) {
|
||||
keys.push({
|
||||
// Use parenthesized substring match if available, index otherwise
|
||||
name: execResult[1] || index++,
|
||||
prefix: "",
|
||||
suffix: "",
|
||||
modifier: "",
|
||||
pattern: ""
|
||||
});
|
||||
execResult = groupsRegex.exec(path.source);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
function arrayToRegexp(paths, keys, options) {
|
||||
var parts = paths.map(function(path) {
|
||||
return pathToRegexp(path, keys, options).source;
|
||||
});
|
||||
return new RegExp("(?:".concat(parts.join("|"), ")"), flags(options));
|
||||
}
|
||||
function stringToRegexp(path, keys, options) {
|
||||
return tokensToRegexp(parse(path, options), keys, options);
|
||||
}
|
||||
function tokensToRegexp(tokens, keys, options) {
|
||||
if (options === void 0) {
|
||||
options = {};
|
||||
}
|
||||
var _a = options.strict, strict = _a === void 0 ? false : _a, _b = options.start, start = _b === void 0 ? true : _b, _c = options.end, end = _c === void 0 ? true : _c, _d = options.encode, encode = _d === void 0 ? function(x) {
|
||||
return x;
|
||||
} : _d, _e = options.delimiter, delimiter = _e === void 0 ? "/#?" : _e, _f = options.endsWith, endsWith = _f === void 0 ? "" : _f;
|
||||
var endsWithRe = "[".concat(escapeString(endsWith), "]|$");
|
||||
var delimiterRe = "[".concat(escapeString(delimiter), "]");
|
||||
var route = start ? "^" : "";
|
||||
for (var _i = 0, tokens_1 = tokens; _i < tokens_1.length; _i++) {
|
||||
var token = tokens_1[_i];
|
||||
if (typeof token === "string") {
|
||||
route += escapeString(encode(token));
|
||||
} else {
|
||||
var prefix = escapeString(encode(token.prefix));
|
||||
var suffix = escapeString(encode(token.suffix));
|
||||
if (token.pattern) {
|
||||
if (keys)
|
||||
keys.push(token);
|
||||
if (prefix || suffix) {
|
||||
if (token.modifier === "+" || token.modifier === "*") {
|
||||
var mod = token.modifier === "*" ? "?" : "";
|
||||
route += "(?:".concat(prefix, "((?:").concat(token.pattern, ")(?:").concat(suffix).concat(prefix, "(?:").concat(token.pattern, "))*)").concat(suffix, ")").concat(mod);
|
||||
} else {
|
||||
route += "(?:".concat(prefix, "(").concat(token.pattern, ")").concat(suffix, ")").concat(token.modifier);
|
||||
}
|
||||
} else {
|
||||
if (token.modifier === "+" || token.modifier === "*") {
|
||||
route += "((?:".concat(token.pattern, ")").concat(token.modifier, ")");
|
||||
} else {
|
||||
route += "(".concat(token.pattern, ")").concat(token.modifier);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
route += "(?:".concat(prefix).concat(suffix, ")").concat(token.modifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (end) {
|
||||
if (!strict)
|
||||
route += "".concat(delimiterRe, "?");
|
||||
route += !options.endsWith ? "$" : "(?=".concat(endsWithRe, ")");
|
||||
} else {
|
||||
var endToken = tokens[tokens.length - 1];
|
||||
var isEndDelimited = typeof endToken === "string" ? delimiterRe.indexOf(endToken[endToken.length - 1]) > -1 : endToken === void 0;
|
||||
if (!strict) {
|
||||
route += "(?:".concat(delimiterRe, "(?=").concat(endsWithRe, "))?");
|
||||
}
|
||||
if (!isEndDelimited) {
|
||||
route += "(?=".concat(delimiterRe, "|").concat(endsWithRe, ")");
|
||||
}
|
||||
}
|
||||
return new RegExp(route, flags(options));
|
||||
}
|
||||
function pathToRegexp(path, keys, options) {
|
||||
if (path instanceof RegExp)
|
||||
return regexpToRegexp(path, keys);
|
||||
if (Array.isArray(path))
|
||||
return arrayToRegexp(path, keys, options);
|
||||
return stringToRegexp(path, keys, options);
|
||||
}
|
||||
|
||||
// ../../../../../../../Library/pnpm/global/5/.pnpm/wrangler@3.65.1/node_modules/wrangler/templates/pages-template-worker.ts
|
||||
var escapeRegex = /[.+?^${}()|[\]\\]/g;
|
||||
function* executeRequest(request) {
|
||||
const requestPath = new URL(request.url).pathname;
|
||||
for (const route of [...routes].reverse()) {
|
||||
if (route.method && route.method !== request.method) {
|
||||
continue;
|
||||
}
|
||||
const routeMatcher = match(route.routePath.replace(escapeRegex, "\\$&"), {
|
||||
end: false
|
||||
});
|
||||
const mountMatcher = match(route.mountPath.replace(escapeRegex, "\\$&"), {
|
||||
end: false
|
||||
});
|
||||
const matchResult = routeMatcher(requestPath);
|
||||
const mountMatchResult = mountMatcher(requestPath);
|
||||
if (matchResult && mountMatchResult) {
|
||||
for (const handler of route.middlewares.flat()) {
|
||||
yield {
|
||||
handler,
|
||||
params: matchResult.params,
|
||||
path: mountMatchResult.path
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const route of routes) {
|
||||
if (route.method && route.method !== request.method) {
|
||||
continue;
|
||||
}
|
||||
const routeMatcher = match(route.routePath.replace(escapeRegex, "\\$&"), {
|
||||
end: true
|
||||
});
|
||||
const mountMatcher = match(route.mountPath.replace(escapeRegex, "\\$&"), {
|
||||
end: false
|
||||
});
|
||||
const matchResult = routeMatcher(requestPath);
|
||||
const mountMatchResult = mountMatcher(requestPath);
|
||||
if (matchResult && mountMatchResult && route.modules.length) {
|
||||
for (const handler of route.modules.flat()) {
|
||||
yield {
|
||||
handler,
|
||||
params: matchResult.params,
|
||||
path: matchResult.path
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
var pages_template_worker_default = {
|
||||
async fetch(originalRequest, env, workerContext) {
|
||||
let request = originalRequest;
|
||||
const handlerIterator = executeRequest(request);
|
||||
let data = {};
|
||||
let isFailOpen = false;
|
||||
const next = async (input, init) => {
|
||||
if (input !== void 0) {
|
||||
let url = input;
|
||||
if (typeof input === "string") {
|
||||
url = new URL(input, request.url).toString();
|
||||
}
|
||||
request = new Request(url, init);
|
||||
}
|
||||
const result = handlerIterator.next();
|
||||
if (result.done === false) {
|
||||
const { handler, params, path } = result.value;
|
||||
const context = {
|
||||
request: new Request(request.clone()),
|
||||
functionPath: path,
|
||||
next,
|
||||
params,
|
||||
get data() {
|
||||
return data;
|
||||
},
|
||||
set data(value) {
|
||||
if (typeof value !== "object" || value === null) {
|
||||
throw new Error("context.data must be an object");
|
||||
}
|
||||
data = value;
|
||||
},
|
||||
env,
|
||||
waitUntil: workerContext.waitUntil.bind(workerContext),
|
||||
passThroughOnException: () => {
|
||||
isFailOpen = true;
|
||||
}
|
||||
};
|
||||
const response = await handler(context);
|
||||
if (!(response instanceof Response)) {
|
||||
throw new Error("Your Pages function should return a Response");
|
||||
}
|
||||
return cloneResponse(response);
|
||||
} else if ("ASSETS") {
|
||||
const response = await env["ASSETS"].fetch(request);
|
||||
return cloneResponse(response);
|
||||
} else {
|
||||
const response = await fetch(request);
|
||||
return cloneResponse(response);
|
||||
}
|
||||
};
|
||||
try {
|
||||
return await next();
|
||||
} catch (error) {
|
||||
if (isFailOpen) {
|
||||
const response = await env["ASSETS"].fetch(request);
|
||||
return cloneResponse(response);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
var cloneResponse = (response) => (
|
||||
// https://fetch.spec.whatwg.org/#null-body-status
|
||||
new Response(
|
||||
[101, 204, 205, 304].includes(response.status) ? null : response.body,
|
||||
response
|
||||
)
|
||||
);
|
||||
export {
|
||||
pages_template_worker_default as default
|
||||
};
|
||||
|
||||
------formdata-undici-075560669371--
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cd "$(dirname ${0})"
|
||||
|
||||
# You should update the version in package.json before running this script
|
||||
FILE="gradio/package.json"
|
||||
new_version=$(python -c "import json; f = open('$FILE', 'r'); data = json.load(f); print(data['version']); f.close();")
|
||||
GRADIO_VERSION=$new_version
|
||||
|
||||
rm -rf gradio/templates/frontend
|
||||
pnpm i --frozen-lockfile --ignore-scripts
|
||||
GRADIO_VERSION=$new_version pnpm build
|
||||
python3 scripts/download_offline_assets.py
|
||||
aws s3 cp gradio/templates/frontend "s3://gradio/${new_version}/" --recursive --region us-west-2
|
||||
|
||||
rm -rf dist/*
|
||||
rm -rf build/*
|
||||
python3 -m build
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,448 @@
|
||||
## JavaScript Client Library
|
||||
|
||||
Interact with Gradio APIs using our JavaScript (and TypeScript) client.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
The Gradio JavaScript Client is available on npm as `@gradio/client`. You can install it as below:
|
||||
|
||||
```shell
|
||||
npm i @gradio/client
|
||||
```
|
||||
|
||||
Or, you can include it directly in your HTML via the jsDelivr CDN:
|
||||
|
||||
```shell
|
||||
<script src="https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"></script>
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The JavaScript Gradio Client exposes the Client class, `Client`, along with various other utility functions. `Client` is used to initialise and establish a connection to, or duplicate, a Gradio app.
|
||||
|
||||
### `Client`
|
||||
|
||||
The Client function connects to the API of a hosted Gradio space and returns an object that allows you to make calls to that API.
|
||||
|
||||
The simplest example looks like this:
|
||||
|
||||
```ts
|
||||
import { Client } from "@gradio/client";
|
||||
|
||||
const app = await Client.connect("user/space-name");
|
||||
const result = await app.predict("/predict");
|
||||
```
|
||||
|
||||
This function accepts two arguments: `source` and `options`:
|
||||
|
||||
#### `source`
|
||||
|
||||
This is the url or name of the gradio app whose API you wish to connect to. This parameter is required and should always be a string. For example:
|
||||
|
||||
```ts
|
||||
Client.connect("user/space-name");
|
||||
```
|
||||
|
||||
#### `options`
|
||||
|
||||
The options object can optionally be passed a second parameter. This object has two properties, `token` and `status_callback`.
|
||||
|
||||
##### `token`
|
||||
|
||||
This should be a Hugging Face personal access token and is required if you wish to make calls to a private gradio api. This option is optional and should be a string starting with `"hf_"`.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
import { Client } from "@gradio/client";
|
||||
|
||||
const app = await Client.connect("user/space-name", { token: "hf_..." });
|
||||
```
|
||||
|
||||
##### `status_callback`
|
||||
|
||||
This should be a function which will notify you of the status of a space if it is not running. If the gradio API you are connecting to is not awake and running or is not hosted on Hugging Face space then this function will do nothing.
|
||||
|
||||
**Additional context**
|
||||
|
||||
Applications hosted on Hugging Face spaces can be in a number of different states. As spaces are a GitOps tool and will rebuild when new changes are pushed to the repository, they have various building, running and error states. If a space is not 'running' then the function passed as the `status_callback` will notify you of the current state of the space and the status of the space as it changes. Spaces that are building or sleeping can take longer than usual to respond, so you can use this information to give users feedback about the progress of their action.
|
||||
|
||||
```ts
|
||||
import { Client, type SpaceStatus } from "@gradio/client";
|
||||
|
||||
const app = await Client.connect("user/space-name", {
|
||||
// The space_status parameter does not need to be manually annotated, this is just for illustration.
|
||||
space_status: (space_status: SpaceStatus) => console.log(space_status)
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
interface SpaceStatusNormal {
|
||||
status: "sleeping" | "running" | "building" | "error" | "stopped";
|
||||
detail:
|
||||
| "SLEEPING"
|
||||
| "RUNNING"
|
||||
| "RUNNING_BUILDING"
|
||||
| "BUILDING"
|
||||
| "NOT_FOUND";
|
||||
load_status: "pending" | "error" | "complete" | "generating";
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface SpaceStatusError {
|
||||
status: "space_error";
|
||||
detail: "NO_APP_FILE" | "CONFIG_ERROR" | "BUILD_ERROR" | "RUNTIME_ERROR";
|
||||
load_status: "error";
|
||||
message: string;
|
||||
discussions_enabled: boolean;
|
||||
|
||||
type SpaceStatus = SpaceStatusNormal | SpaceStatusError;
|
||||
```
|
||||
|
||||
The gradio client returns an object with a number of methods and properties:
|
||||
|
||||
#### `predict`
|
||||
|
||||
The `predict` method allows you to call an api endpoint and get a prediction result:
|
||||
|
||||
```ts
|
||||
import { Client } from "@gradio/client";
|
||||
|
||||
const app = await Client.connect("user/space-name");
|
||||
const result = await app.predict("/predict");
|
||||
```
|
||||
|
||||
`predict` accepts two parameters, `endpoint` and `payload`. It returns a promise that resolves to the prediction result.
|
||||
|
||||
##### `endpoint`
|
||||
|
||||
This is the endpoint for an api request and is required. The default endpoint for a `gradio.Interface` is `"/predict"`. Explicitly named endpoints have a custom name. The endpoint names can be found on the "View API" page of a space.
|
||||
|
||||
```ts
|
||||
import { Client } from "@gradio/client";
|
||||
|
||||
const app = await Client.connect("user/space-name");
|
||||
const result = await app.predict("/predict");
|
||||
```
|
||||
|
||||
##### `payload`
|
||||
|
||||
The `payload` argument is generally required but this depends on the API itself. If the API endpoint depends on values being passed in then the argument is required for the API request to succeed. The data that should be passed in is detailed on the "View API" page of a space, or accessible via the `view_api()` method of the client.
|
||||
|
||||
```ts
|
||||
import { Client } from "@gradio/client";
|
||||
|
||||
const app = await Client.connect("user/space-name");
|
||||
const result = await app.predict("/predict", {
|
||||
input: 1,
|
||||
word_1: "Hello",
|
||||
word_2: "friends"
|
||||
});
|
||||
```
|
||||
|
||||
#### `submit`
|
||||
|
||||
The `submit` method provides a more flexible way to call an API endpoint, providing you with status updates about the current progress of the prediction as well as supporting more complex endpoint types.
|
||||
|
||||
```ts
|
||||
import { Client } from "@gradio/client";
|
||||
|
||||
const app = await Client.connect("user/space-name");
|
||||
const submission = app.submit("/predict", { name: "Chewbacca" });
|
||||
```
|
||||
|
||||
The `submit` method accepts the same [`endpoint`](#endpoint) and [`payload`](#payload) arguments as `predict`.
|
||||
|
||||
The `submit` method does not return a promise and should not be awaited, instead it returns an async iterator with a `cancel` method.
|
||||
|
||||
##### Accessing values
|
||||
|
||||
Iterating the submission allows you to access the events related to the submitted API request. There are two types of events that can be listened for: `"data"` updates and `"status"` updates. By default only the `"data"` event is reported, but you can listen for the `"status"` event by manually passing the events you care about when instantiating the client:
|
||||
|
||||
```ts
|
||||
import { Client } from "@gradio/client";
|
||||
|
||||
const app = await Client.connect("user/space-name", {
|
||||
events: ["data", "status"]
|
||||
});
|
||||
```
|
||||
|
||||
`"data"` updates are issued when the API computes a value, the callback provided as the second argument will be called when such a value is sent to the client. The shape of the data depends on the way the API itself is constructed. This event may fire more than once if that endpoint supports emmitting new values over time.
|
||||
|
||||
`"status` updates are issued when the status of a request changes. This information allows you to offer feedback to users when the queue position of the request changes, or when the request changes from queued to processing.
|
||||
|
||||
The status payload look like this:
|
||||
|
||||
```ts
|
||||
interface Status {
|
||||
queue: boolean;
|
||||
code?: string;
|
||||
success?: boolean;
|
||||
stage: "pending" | "error" | "complete" | "generating";
|
||||
size?: number;
|
||||
position?: number;
|
||||
eta?: number;
|
||||
message?: string;
|
||||
progress_data?: Array<{
|
||||
progress: number | null;
|
||||
index: number | null;
|
||||
length: number | null;
|
||||
unit: string | null;
|
||||
desc: string | null;
|
||||
}>;
|
||||
time?: Date;
|
||||
}
|
||||
```
|
||||
|
||||
Usage looks like this:
|
||||
|
||||
```ts
|
||||
import { Client } from "@gradio/client";
|
||||
|
||||
const app = await Client.connect("user/space-name");
|
||||
const submission = app
|
||||
.submit("/predict", { name: "Chewbacca" })
|
||||
|
||||
for await (const msg of submission) {
|
||||
if (msg.type === "data") {
|
||||
console.log(msg.data);
|
||||
}
|
||||
|
||||
if (msg.type === "status") {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### `cancel`
|
||||
|
||||
Certain types of gradio function can run repeatedly and in some cases indefinitely. the `cancel` method will stop such an endpoints and prevent the API from issuing additional updates.
|
||||
|
||||
```ts
|
||||
import { Client } from "@gradio/client";
|
||||
|
||||
const app = await Client.connect("user/space-name");
|
||||
const submission = app
|
||||
.submit("/predict", { name: "Chewbacca" })
|
||||
|
||||
|
||||
// later
|
||||
|
||||
submission.cancel();
|
||||
```
|
||||
|
||||
#### `view_api`
|
||||
|
||||
The `view_api` method provides details about the API you are connected to. It returns a JavaScript object of all named endpoints, unnamed endpoints and what values they accept and return. This method does not accept arguments.
|
||||
|
||||
```ts
|
||||
import { Client } from "@gradio/client";
|
||||
|
||||
const app = await Client.connect("user/space-name");
|
||||
const api_info = await app.view_api();
|
||||
|
||||
console.log(api_info);
|
||||
```
|
||||
|
||||
#### `config`
|
||||
|
||||
The `config` property contains the configuration for the gradio application you are connected to. This object may contain useful meta information about the application.
|
||||
|
||||
```ts
|
||||
import { Client } from "@gradio/client";
|
||||
|
||||
const app = await Client.connect("user/space-name");
|
||||
console.log(app.config);
|
||||
```
|
||||
|
||||
### `duplicate`
|
||||
|
||||
The duplicate function will attempt to duplicate the space that is referenced and return an instance of `client` connected to that space. If the space has already been duplicated then it will not create a new duplicate and will instead connect to the existing duplicated space. The huggingface token that is passed in will dictate the user under which the space is created.
|
||||
|
||||
`duplicate` accepts the same arguments as `client` with the addition of a `private` options property dictating whether the duplicated space should be private or public. A huggingface token is required for duplication to work.
|
||||
|
||||
```ts
|
||||
import { Client } from "@gradio/client";
|
||||
|
||||
const app = await Client.duplicate("user/space-name", {
|
||||
token: "hf_..."
|
||||
});
|
||||
```
|
||||
|
||||
This function accepts two arguments: `source` and `options`:
|
||||
|
||||
#### `source`
|
||||
|
||||
The space to duplicate and connect to. [See `client`'s `source` parameter](#source).
|
||||
|
||||
#### `options`
|
||||
|
||||
Accepts all options that `client` accepts, except `token` is required. [See `client`'s `options` parameter](#source).
|
||||
|
||||
`duplicate` also accepts one additional `options` property.
|
||||
|
||||
##### `private`
|
||||
|
||||
This is an optional property specific to `duplicate`'s options object and will determine whether the space should be public or private. Spaces duplicated via the `duplicate` method are public by default.
|
||||
|
||||
```ts
|
||||
import { Client } from "@gradio/client";
|
||||
|
||||
const app = await Client.duplicate("user/space-name", {
|
||||
token: "hf_...",
|
||||
private: true
|
||||
});
|
||||
```
|
||||
|
||||
##### `timeout`
|
||||
|
||||
This is an optional property specific to `duplicate`'s options object and will set the timeout in minutes before the duplicated space will go to sleep.
|
||||
|
||||
```ts
|
||||
import { Client } from "@gradio/client";
|
||||
|
||||
const app = await Client.duplicate("user/space-name", {
|
||||
token: "hf_...",
|
||||
private: true,
|
||||
timeout: 5
|
||||
});
|
||||
```
|
||||
|
||||
##### `hardware`
|
||||
|
||||
This is an optional property specific to `duplicate`'s options object and will set the hardware for the duplicated space. By default the hardware used will match that of the original space. If this cannot be obtained it will default to `"cpu-basic"`. For hardware upgrades (beyond the basic CPU tier), you may be required to provide [billing information on Hugging Face](https://huggingface.co/settings/billing).
|
||||
|
||||
Possible hardware options are:
|
||||
|
||||
- `"cpu-basic"`
|
||||
- `"cpu-upgrade"`
|
||||
- `"cpu-xl"`
|
||||
- `"t4-small"`
|
||||
- `"t4-medium"`
|
||||
- `"a10g-small"`
|
||||
- `"a10g-large"`
|
||||
- `"a10g-largex2"`
|
||||
- `"a10g-largex4"`
|
||||
- `"a100-large"`
|
||||
- `"zero-a10g"`
|
||||
- `"h100"`
|
||||
- `"h100x8"`
|
||||
|
||||
```ts
|
||||
import { Client } from "@gradio/client";
|
||||
|
||||
const app = await Client.duplicate("user/space-name", {
|
||||
token: "hf_...",
|
||||
private: true,
|
||||
hardware: "a10g-small"
|
||||
});
|
||||
```
|
||||
|
||||
### `handle_file(file_or_url: File | string | Blob | Buffer)`
|
||||
|
||||
This utility function is used to simplify the process of handling file inputs for the client.
|
||||
|
||||
Gradio APIs expect a special file datastructure that references a location on the server. These files can be manually uploaded but figuring what to do with different file types can be difficult depending on your environment.
|
||||
|
||||
This function will handle files regardless of whether or not they are local files (node only), URLs, Blobs, or Buffers. It will take in a reference and handle it accordingly,uploading the file where appropriate and generating the correct data structure for the client.
|
||||
|
||||
The return value of this function can be used anywhere in the input data where a file is expected:
|
||||
|
||||
```ts
|
||||
import { handle_file } from "@gradio/client";
|
||||
|
||||
const app = await Client.connect("user/space-name");
|
||||
const result = await app.predict("/predict", {
|
||||
single: handle_file(file),
|
||||
flat: [handle_file(url), handle_file(buffer)],
|
||||
nested: {
|
||||
image: handle_file(url),
|
||||
layers: [handle_file(buffer)]
|
||||
},
|
||||
deeply_nested: {
|
||||
image: handle_file(url),
|
||||
layers: [{
|
||||
layer1: handle_file(buffer),
|
||||
layer2: handle_file(buffer)
|
||||
}]
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### filepaths
|
||||
|
||||
`handle_file` can be passed a local filepath which it will upload to the client server and return a reference that the client can understand.
|
||||
|
||||
This only works in a node environment.
|
||||
|
||||
Filepaths are resolved relative to the current working directory, not the location of the file that calls `handle_file`.
|
||||
|
||||
```ts
|
||||
import { handle_file } from "@gradio/client";
|
||||
|
||||
// not uploaded yet
|
||||
const file_ref = handle_file("path/to/file");
|
||||
|
||||
const app = await Client.connect("user/space-name");
|
||||
|
||||
// upload happens here
|
||||
const result = await app.predict("/predict", {
|
||||
file: file_ref,
|
||||
});
|
||||
```
|
||||
|
||||
#### URLs
|
||||
|
||||
`handle_file` can be passed a URL which it will convert into a reference that the client can understand.
|
||||
|
||||
```ts
|
||||
import { handle_file } from "@gradio/client";
|
||||
|
||||
const url_ref = handle_file("https://example.com/file.png");
|
||||
|
||||
const app = await Client.connect("user/space-name");
|
||||
const result = await app.predict("/predict", {
|
||||
url: url_ref,
|
||||
});
|
||||
```
|
||||
|
||||
#### Blobs
|
||||
|
||||
`handle_file` can be passed a Blob which it will upload to the client server and return a reference that the client can understand.
|
||||
|
||||
The upload is not initiated until predict or submit are called.
|
||||
|
||||
```ts
|
||||
import { handle_file } from "@gradio/client";
|
||||
|
||||
// not uploaded yet
|
||||
const blob_ref = handle_file(new Blob(["Hello, world!"]));
|
||||
|
||||
const app = await Client.connect("user/space-name");
|
||||
|
||||
// upload happens here
|
||||
const result = await app.predict("/predict", {
|
||||
blob: blob_ref,
|
||||
});
|
||||
```
|
||||
|
||||
#### Buffers
|
||||
|
||||
`handle_file` can be passed a Buffer which it will upload to the client server and return a reference that the client can understand.
|
||||
|
||||
```ts
|
||||
import { handle_file } from "@gradio/client";
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
// not uploaded yet
|
||||
const buffer_ref = handle_file(readFileSync("file.png"));
|
||||
|
||||
const app = await Client.connect("user/space-name");
|
||||
|
||||
// upload happens here
|
||||
const result = await app.predict("/predict", {
|
||||
buffer: buffer_ref,
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,39 @@
|
||||
<!doctype html>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Client</title>
|
||||
<script type="module">
|
||||
import { Client } from "./dist/index.js";
|
||||
console.log(Client);
|
||||
|
||||
const client = await Client.connect("pngwn/chatinterface_streaming_echo");
|
||||
async function run(message, n) {
|
||||
// console.log(client);
|
||||
const req = client.submit("/chat", {
|
||||
message
|
||||
});
|
||||
console.log("start");
|
||||
for await (const c of req) {
|
||||
if (c.type === "data") {
|
||||
console.log(`${n}: ${c.data[0]}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("end");
|
||||
|
||||
return "hi";
|
||||
}
|
||||
|
||||
run("My name is frank", 1);
|
||||
run("Hello there", 2);
|
||||
|
||||
console.log("boo");
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+2093
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@gradio/client",
|
||||
"version": "2.3.1",
|
||||
"description": "Gradio API client",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"exports": {
|
||||
".": {
|
||||
"gradio": "./src/index.ts",
|
||||
"browser": "./dist/browser.js",
|
||||
"import": "./dist/index.js",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./package.json": "./package.json",
|
||||
"./browser.js": "./dist/browser.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"fetch-event-stream": "^0.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"msw": "^2.11.3",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"scripts": {
|
||||
"bundle": "vite build --ssr",
|
||||
"bundle:browser": "BROWSER_BUILD=true vite build",
|
||||
"generate_types": "tsc",
|
||||
"clean": "rm -rf dist",
|
||||
"build": "pnpm clean && pnpm bundle && pnpm bundle:browser && pnpm generate_types",
|
||||
"test": "pnpm test:browser && pnpm test:node",
|
||||
"test:browser": "vitest run -c vite.config.ts",
|
||||
"test:node": "NODE_NO_WARNINGS=1 TEST_MODE=node vitest run -c vite.config.ts",
|
||||
"preview:browser": "vite dev --mode=preview"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"main_changeset": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/gradio-app/gradio.git",
|
||||
"directory": "client/js"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,552 @@
|
||||
import type {
|
||||
ApiData,
|
||||
ApiInfo,
|
||||
ClientOptions,
|
||||
Config,
|
||||
DuplicateOptions,
|
||||
EndpointInfo,
|
||||
JsApiData,
|
||||
PredictReturn,
|
||||
SpaceStatus,
|
||||
Status,
|
||||
UploadResponse,
|
||||
client_return,
|
||||
SubmitIterable,
|
||||
GradioEvent
|
||||
} from "./types";
|
||||
import { view_api } from "./utils/view_api";
|
||||
import { upload_files } from "./utils/upload_files";
|
||||
import { upload, FileData } from "./upload";
|
||||
import { handle_blob } from "./utils/handle_blob";
|
||||
import { post_data } from "./utils/post_data";
|
||||
import { predict } from "./utils/predict";
|
||||
import { duplicate } from "./utils/duplicate";
|
||||
import { submit } from "./utils/submit";
|
||||
import { RE_SPACE_NAME, process_endpoint } from "./helpers/api_info";
|
||||
import {
|
||||
map_names_to_ids,
|
||||
resolve_cookies,
|
||||
resolve_config,
|
||||
get_jwt,
|
||||
parse_and_set_cookies
|
||||
} from "./helpers/init_helpers";
|
||||
import { check_and_wake_space, check_space_status } from "./helpers/spaces";
|
||||
import { initialize_zerogpu_handshake } from "./helpers/zerogpu";
|
||||
import { open_stream, readable_stream, close_stream } from "./utils/stream";
|
||||
import {
|
||||
API_INFO_ERROR_MSG,
|
||||
APP_ID_URL,
|
||||
CONFIG_ERROR_MSG,
|
||||
HEARTBEAT_URL,
|
||||
COMPONENT_SERVER_URL
|
||||
} from "./constants";
|
||||
declare const BROWSER_BUILD: boolean;
|
||||
|
||||
export class Client {
|
||||
app_reference: string;
|
||||
options: ClientOptions;
|
||||
deep_link: string | null = null;
|
||||
|
||||
config: Config | undefined;
|
||||
api_prefix = "";
|
||||
api_info: ApiInfo<JsApiData> | undefined;
|
||||
api_map: Record<string, number> = {};
|
||||
session_hash: string = Math.random().toString(36).substring(2);
|
||||
jwt: string | false = false;
|
||||
last_status: Record<string, Status["stage"]> = {};
|
||||
|
||||
private cookies: string | null = null;
|
||||
|
||||
// streaming
|
||||
stream_status = { open: false };
|
||||
closed = false;
|
||||
pending_stream_messages: Record<string, any[][]> = {};
|
||||
pending_diff_streams: Record<string, any[][]> = {};
|
||||
event_callbacks: Record<string, (data?: unknown) => Promise<void>> = {};
|
||||
unclosed_events: Set<string> = new Set();
|
||||
heartbeat_event: EventSource | null = null;
|
||||
abort_controller: AbortController | null = null;
|
||||
stream_instance: EventSource | null = null;
|
||||
current_payload: any;
|
||||
|
||||
get_url_config(url: string | null = null): Config {
|
||||
if (!this.config) {
|
||||
throw new Error(CONFIG_ERROR_MSG);
|
||||
}
|
||||
if (url === null) {
|
||||
url = window.location.href;
|
||||
}
|
||||
const stripSlashes = (str: string): string => str.replace(/^\/+|\/+$/g, "");
|
||||
let root_path = stripSlashes(new URL(this.config.root).pathname);
|
||||
let url_path = stripSlashes(new URL(url).pathname);
|
||||
let page: string;
|
||||
if (!url_path.startsWith(root_path)) {
|
||||
page = "";
|
||||
} else {
|
||||
page = stripSlashes(url_path.substring(root_path.length));
|
||||
}
|
||||
return this.get_page_config(page);
|
||||
}
|
||||
get_page_config(page: string): Config {
|
||||
if (!this.config) {
|
||||
throw new Error(CONFIG_ERROR_MSG);
|
||||
}
|
||||
let config = this.config;
|
||||
if (!(page in config.page)) {
|
||||
page = "";
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
current_page: page,
|
||||
layout: config.page[page].layout,
|
||||
components: config.components.filter((c) =>
|
||||
config.page[page].components.includes(c.id)
|
||||
),
|
||||
dependencies: this.config.dependencies.filter((d) =>
|
||||
config.page[page].dependencies.includes(d.id)
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
||||
const headers = new Headers(init?.headers || {});
|
||||
if (this && this.cookies) {
|
||||
headers.append("Cookie", this.cookies);
|
||||
}
|
||||
if (this && this.options.headers) {
|
||||
let additional_headers = new Headers(this.options.headers);
|
||||
|
||||
additional_headers.forEach((value, name) => {
|
||||
headers.append(name, value);
|
||||
});
|
||||
}
|
||||
|
||||
return fetch(input, { ...init, headers });
|
||||
}
|
||||
|
||||
stream(url: URL): EventSource {
|
||||
const headers = new Headers();
|
||||
if (this && this.cookies) {
|
||||
headers.append("Cookie", this.cookies);
|
||||
}
|
||||
if (this && this.options.headers) {
|
||||
let additional_headers = new Headers(this.options.headers);
|
||||
|
||||
additional_headers.forEach((value, name) => {
|
||||
headers.append(name, value);
|
||||
});
|
||||
}
|
||||
if (this && this.options.token) {
|
||||
headers.append("Authorization", `Bearer ${this.options.token}`);
|
||||
}
|
||||
|
||||
this.abort_controller = new AbortController();
|
||||
|
||||
this.stream_instance = readable_stream(url.toString(), {
|
||||
credentials: this.options.credentials ?? "same-origin",
|
||||
headers: headers,
|
||||
signal: this.abort_controller.signal
|
||||
});
|
||||
|
||||
return this.stream_instance;
|
||||
}
|
||||
|
||||
view_api: () => Promise<ApiInfo<JsApiData>>;
|
||||
upload_files: (
|
||||
root_url: string,
|
||||
files: (Blob | File)[],
|
||||
upload_id?: string
|
||||
) => Promise<UploadResponse>;
|
||||
upload: (
|
||||
file_data: FileData[],
|
||||
root_url: string,
|
||||
upload_id?: string,
|
||||
max_file_size?: number
|
||||
) => Promise<(FileData | null)[] | null>;
|
||||
handle_blob: (
|
||||
endpoint: string,
|
||||
data: unknown[],
|
||||
endpoint_info: EndpointInfo<ApiData | JsApiData>
|
||||
) => Promise<unknown[]>;
|
||||
post_data: (
|
||||
url: string,
|
||||
body: unknown,
|
||||
additional_headers?: any
|
||||
) => Promise<unknown[]>;
|
||||
submit: (
|
||||
endpoint: string | number,
|
||||
data: unknown[] | Record<string, unknown> | undefined,
|
||||
event_data?: unknown,
|
||||
trigger_id?: number | null,
|
||||
all_events?: boolean
|
||||
) => SubmitIterable<GradioEvent>;
|
||||
predict: <T = unknown>(
|
||||
endpoint: string | number,
|
||||
data: unknown[] | Record<string, unknown> | undefined,
|
||||
event_data?: unknown
|
||||
) => Promise<PredictReturn<T>>;
|
||||
open_stream: () => Promise<void>;
|
||||
private resolve_config: (endpoint: string) => Promise<Config | undefined>;
|
||||
private resolve_cookies: () => Promise<void>;
|
||||
constructor(
|
||||
app_reference: string,
|
||||
options: ClientOptions = { events: ["data"] }
|
||||
) {
|
||||
this.app_reference = app_reference;
|
||||
this.deep_link = options.query_params?.deep_link || null;
|
||||
if (!options.events) {
|
||||
options.events = ["data"];
|
||||
}
|
||||
|
||||
this.options = options;
|
||||
this.current_payload = {};
|
||||
|
||||
if (options.cookies) {
|
||||
this.cookies = options.cookies;
|
||||
}
|
||||
|
||||
this.view_api = view_api.bind(this);
|
||||
this.upload_files = upload_files.bind(this);
|
||||
this.handle_blob = handle_blob.bind(this);
|
||||
this.post_data = post_data.bind(this);
|
||||
this.submit = submit.bind(this);
|
||||
this.predict = predict.bind(this) as typeof this.predict;
|
||||
this.open_stream = open_stream.bind(this);
|
||||
this.resolve_config = resolve_config.bind(this);
|
||||
this.resolve_cookies = resolve_cookies.bind(this);
|
||||
this.upload = upload.bind(this);
|
||||
this.fetch = this.fetch.bind(this);
|
||||
this.handle_space_success = this.handle_space_success.bind(this);
|
||||
this.stream = this.stream.bind(this);
|
||||
}
|
||||
|
||||
private async init(): Promise<void> {
|
||||
initialize_zerogpu_handshake();
|
||||
|
||||
if (this.options.auth) {
|
||||
await this.resolve_cookies();
|
||||
}
|
||||
|
||||
await this._resolve_config().then(({ config }) =>
|
||||
this._resolve_heartbeat(config)
|
||||
);
|
||||
|
||||
this.api_info = await this.view_api();
|
||||
this.api_map = map_names_to_ids(this.config?.dependencies || []);
|
||||
}
|
||||
|
||||
async _resolve_heartbeat(_config: Config): Promise<void> {
|
||||
if (_config) {
|
||||
this.config = _config;
|
||||
this.api_prefix = _config.api_prefix || "";
|
||||
|
||||
if (this.config && this.config.connect_heartbeat) {
|
||||
if (this.config.space_id && this.options.token) {
|
||||
this.jwt = await get_jwt(
|
||||
this.config.space_id,
|
||||
this.options.token,
|
||||
this.cookies
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_config.space_id && this.options.token) {
|
||||
this.jwt = await get_jwt(_config.space_id, this.options.token);
|
||||
}
|
||||
|
||||
if (this.config && this.config.connect_heartbeat) {
|
||||
// connect to the heartbeat endpoint via GET request
|
||||
const heartbeat_url = new URL(
|
||||
`${this.config.root}${this.api_prefix}/${HEARTBEAT_URL}/${this.session_hash}`
|
||||
);
|
||||
|
||||
// if the jwt is available, add it to the query params
|
||||
if (this.jwt) {
|
||||
heartbeat_url.searchParams.set("__sign", this.jwt);
|
||||
}
|
||||
|
||||
// Just connect to the endpoint without parsing the response. Ref: https://github.com/gradio-app/gradio/pull/7974#discussion_r1557717540
|
||||
if (!this.heartbeat_event) {
|
||||
this.heartbeat_event = this.stream(heartbeat_url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async connect(
|
||||
app_reference: string,
|
||||
options: ClientOptions = {
|
||||
events: ["data"]
|
||||
}
|
||||
): Promise<Client> {
|
||||
const client = new this(app_reference, options); // this refers to the class itself, not the instance
|
||||
if (options.session_hash) {
|
||||
client.session_hash = options.session_hash;
|
||||
}
|
||||
await client.init();
|
||||
return client;
|
||||
}
|
||||
|
||||
async reconnect(): Promise<"connected" | "broken" | "changed"> {
|
||||
const app_id_url = new URL(
|
||||
`${this.config!.root}${this.api_prefix}/${APP_ID_URL}`
|
||||
);
|
||||
let app_id: string;
|
||||
try {
|
||||
const response = await this.fetch(app_id_url);
|
||||
if (!response.ok) {
|
||||
throw new Error();
|
||||
}
|
||||
app_id = ((await response.json()) as any).app_id;
|
||||
} catch (e) {
|
||||
return "broken";
|
||||
}
|
||||
if (app_id !== this.config!.app_id) {
|
||||
return "changed";
|
||||
}
|
||||
return "connected";
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.closed = true;
|
||||
close_stream(this.stream_status, this.abort_controller);
|
||||
}
|
||||
|
||||
set_current_payload(payload: any): void {
|
||||
this.current_payload = payload;
|
||||
}
|
||||
|
||||
static async duplicate(
|
||||
app_reference: string,
|
||||
options: DuplicateOptions = {
|
||||
events: ["data"]
|
||||
}
|
||||
): Promise<Client> {
|
||||
return duplicate(app_reference, options);
|
||||
}
|
||||
|
||||
private async _resolve_config(): Promise<any> {
|
||||
const { http_protocol, host, space_id } = await process_endpoint(
|
||||
this.app_reference,
|
||||
this.options.token
|
||||
);
|
||||
|
||||
const { status_callback } = this.options;
|
||||
|
||||
if (space_id && status_callback) {
|
||||
await check_and_wake_space(space_id, status_callback);
|
||||
}
|
||||
|
||||
let config: Config | undefined;
|
||||
|
||||
try {
|
||||
// Create base URL
|
||||
let configUrl = `${http_protocol}//${host}`;
|
||||
config = await this.resolve_config(configUrl);
|
||||
|
||||
if (!config) {
|
||||
throw new Error(CONFIG_ERROR_MSG);
|
||||
}
|
||||
|
||||
return this.config_success(config);
|
||||
} catch (e: any) {
|
||||
if (space_id && status_callback) {
|
||||
check_space_status(
|
||||
space_id,
|
||||
RE_SPACE_NAME.test(space_id) ? "space_name" : "subdomain",
|
||||
this.handle_space_success
|
||||
);
|
||||
} else {
|
||||
if (status_callback)
|
||||
status_callback({
|
||||
status: "error",
|
||||
message: "Could not load this space.",
|
||||
load_status: "error",
|
||||
detail: "NOT_FOUND"
|
||||
});
|
||||
throw Error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async config_success(
|
||||
_config: Config
|
||||
): Promise<Config | client_return> {
|
||||
this.config = _config;
|
||||
this.api_prefix = _config.api_prefix || "";
|
||||
|
||||
if (this.config.auth_required) {
|
||||
return this.prepare_return_obj();
|
||||
}
|
||||
|
||||
try {
|
||||
this.api_info = await this.view_api();
|
||||
} catch (e) {
|
||||
console.error(API_INFO_ERROR_MSG + (e as Error).message);
|
||||
}
|
||||
|
||||
return this.prepare_return_obj();
|
||||
}
|
||||
|
||||
async handle_space_success(status: SpaceStatus): Promise<Config | void> {
|
||||
if (!this) {
|
||||
throw new Error(CONFIG_ERROR_MSG);
|
||||
}
|
||||
const { status_callback } = this.options;
|
||||
if (status_callback) status_callback(status);
|
||||
if (status.status === "running") {
|
||||
try {
|
||||
this.config = await this._resolve_config();
|
||||
this.api_prefix = this?.config?.api_prefix || "";
|
||||
|
||||
if (!this.config) {
|
||||
throw new Error(CONFIG_ERROR_MSG);
|
||||
}
|
||||
|
||||
const _config = await this.config_success(this.config);
|
||||
|
||||
return _config as Config;
|
||||
} catch (e) {
|
||||
if (status_callback) {
|
||||
status_callback({
|
||||
status: "error",
|
||||
message: "Could not load this space.",
|
||||
load_status: "error",
|
||||
detail: "NOT_FOUND"
|
||||
});
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async component_server(
|
||||
component_id: number,
|
||||
fn_name: string,
|
||||
data: unknown | { binary: boolean; data: Record<string, any> }
|
||||
): Promise<unknown> {
|
||||
if (!this.config) {
|
||||
throw new Error(CONFIG_ERROR_MSG);
|
||||
}
|
||||
|
||||
const headers: {
|
||||
Authorization?: string;
|
||||
"Content-Type"?: "application/json";
|
||||
} = {};
|
||||
|
||||
const { token } = this.options;
|
||||
const { session_hash } = this;
|
||||
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${this.options.token}`;
|
||||
}
|
||||
|
||||
let root_url: string;
|
||||
let component = this.config.components.find(
|
||||
(comp) => comp.id === component_id
|
||||
);
|
||||
if (component?.props?.root_url) {
|
||||
root_url = component.props.root_url;
|
||||
} else {
|
||||
root_url = this.config.root;
|
||||
}
|
||||
|
||||
let body: FormData | string;
|
||||
|
||||
if (typeof data === "object" && data !== null && "binary" in data) {
|
||||
const _data = data as { binary: boolean; data: Record<string, any> };
|
||||
body = new FormData();
|
||||
for (const key in _data.data) {
|
||||
if (key === "binary") continue;
|
||||
body.append(key, _data.data[key]);
|
||||
}
|
||||
body.set("component_id", component_id.toString());
|
||||
body.set("fn_name", fn_name);
|
||||
body.set("session_hash", session_hash);
|
||||
} else {
|
||||
body = JSON.stringify({
|
||||
data: data,
|
||||
component_id,
|
||||
fn_name,
|
||||
session_hash
|
||||
});
|
||||
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.fetch(
|
||||
`${root_url}${this.api_prefix}/${COMPONENT_SERVER_URL}/`,
|
||||
{
|
||||
method: "POST",
|
||||
body: body,
|
||||
headers,
|
||||
credentials: this.options.credentials ?? "same-origin"
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
"Could not connect to component server: " + response.statusText
|
||||
);
|
||||
}
|
||||
|
||||
const output = await response.json();
|
||||
return output;
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
public set_cookies(raw_cookies: string): void {
|
||||
this.cookies = parse_and_set_cookies(raw_cookies).join("; ");
|
||||
}
|
||||
|
||||
private prepare_return_obj(): client_return {
|
||||
return {
|
||||
config: this.config,
|
||||
predict: this.predict,
|
||||
submit: this.submit,
|
||||
view_api: this.view_api,
|
||||
component_server: this.component_server
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in v1.0. Use `Client.connect()` instead.
|
||||
* Creates a client instance for interacting with Gradio apps.
|
||||
*
|
||||
* @param {string} app_reference - The reference or URL to a Gradio space or app.
|
||||
* @param {ClientOptions} options - Configuration options for the client.
|
||||
* @returns {Promise<Client>} A promise that resolves to a `Client` instance.
|
||||
*/
|
||||
export async function client(
|
||||
app_reference: string,
|
||||
options: ClientOptions = {
|
||||
events: ["data"]
|
||||
}
|
||||
): Promise<Client> {
|
||||
return await Client.connect(app_reference, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in v1.0. Use `Client.duplicate()` instead.
|
||||
* Creates a duplicate of a space and returns a client instance for the duplicated space.
|
||||
*
|
||||
* @param {string} app_reference - The reference or URL to a Gradio space or app to duplicate.
|
||||
* @param {DuplicateOptions} options - Configuration options for the client.
|
||||
* @returns {Promise<Client>} A promise that resolves to a `Client` instance.
|
||||
*/
|
||||
export async function duplicate_space(
|
||||
app_reference: string,
|
||||
options: DuplicateOptions
|
||||
): Promise<Client> {
|
||||
return await Client.duplicate(app_reference, options);
|
||||
}
|
||||
|
||||
export type ClientInstance = Client;
|
||||
@@ -0,0 +1,41 @@
|
||||
// endpoints
|
||||
export const HOST_URL = `host`;
|
||||
export const API_URL = `predict/`;
|
||||
export const SSE_URL_V0 = `queue/join`;
|
||||
export const SSE_DATA_URL_V0 = `queue/data`;
|
||||
export const SSE_URL = `queue/data`;
|
||||
export const SSE_DATA_URL = `queue/join`;
|
||||
export const UPLOAD_URL = `upload`;
|
||||
export const LOGIN_URL = `login`;
|
||||
export const CONFIG_URL = `config`;
|
||||
export const API_INFO_URL = `info`;
|
||||
export const RUNTIME_URL = `runtime`;
|
||||
export const SLEEPTIME_URL = `sleeptime`;
|
||||
export const HEARTBEAT_URL = `heartbeat`;
|
||||
export const COMPONENT_SERVER_URL = `component_server`;
|
||||
export const RESET_URL = `reset`;
|
||||
export const CANCEL_URL = `cancel`;
|
||||
export const APP_ID_URL = `app_id`;
|
||||
|
||||
export const RAW_API_INFO_URL = `info?serialize=False`;
|
||||
export const SPACE_FETCHER_URL =
|
||||
"https://gradio-space-api-fetcher-v2.hf.space/api";
|
||||
export const SPACE_URL = "https://hf.space/{}";
|
||||
|
||||
// messages
|
||||
export const QUEUE_FULL_MSG =
|
||||
"This application is currently busy. Please try again. ";
|
||||
export const BROKEN_CONNECTION_MSG = "Connection errored out. ";
|
||||
export const CONFIG_ERROR_MSG = "Could not resolve app config. ";
|
||||
export const SPACE_STATUS_ERROR_MSG = "Could not get space status. ";
|
||||
export const API_INFO_ERROR_MSG = "Could not get API info. ";
|
||||
export const SPACE_METADATA_ERROR_MSG = "Space metadata could not be loaded. ";
|
||||
export const INVALID_URL_MSG = "Invalid URL. A full URL path is required.";
|
||||
export const UNAUTHORIZED_MSG = "Not authorized to access this space. ";
|
||||
export const INVALID_CREDENTIALS_MSG = "Invalid credentials. Could not login. ";
|
||||
export const MISSING_CREDENTIALS_MSG =
|
||||
"Login credentials are required to access this space.";
|
||||
export const NODEJS_FS_ERROR_MSG =
|
||||
"File system access is only available in Node.js environments";
|
||||
export const ROOT_URL_ERROR_MSG = "Root URL not found in client config";
|
||||
export const FILE_PROCESSING_ERROR_MSG = "Error uploading file";
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
import { ApiData, ApiInfo, Config } from "./types";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__gradio_mode__: "app" | "website";
|
||||
gradio_config: Config;
|
||||
gradio_api_info: ApiInfo<ApiData> | { api: ApiInfo<ApiData> };
|
||||
__is_colab__: boolean;
|
||||
__gradio_space__: string | null;
|
||||
supports_zerogpu_headers?: boolean;
|
||||
BUILD_MODE?: "dev" | "production";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
import {
|
||||
HOST_URL,
|
||||
INVALID_URL_MSG,
|
||||
QUEUE_FULL_MSG,
|
||||
SPACE_METADATA_ERROR_MSG
|
||||
} from "../constants";
|
||||
import type {
|
||||
ApiData,
|
||||
ApiInfo,
|
||||
Config,
|
||||
JsApiData,
|
||||
EndpointInfo,
|
||||
Status
|
||||
} from "../types";
|
||||
import { determine_protocol } from "./init_helpers";
|
||||
|
||||
export const RE_SPACE_NAME = /^[a-zA-Z0-9_\-\.]+\/[a-zA-Z0-9_\-\.]+$/;
|
||||
export const RE_SPACE_DOMAIN = /.*hf\.space\/{0,1}.*$/;
|
||||
|
||||
export async function process_endpoint(
|
||||
app_reference: string,
|
||||
token?: `hf_${string}`
|
||||
): Promise<{
|
||||
space_id: string | false;
|
||||
host: string;
|
||||
ws_protocol: "ws" | "wss";
|
||||
http_protocol: "http:" | "https:";
|
||||
}> {
|
||||
const headers: { Authorization?: string } = {};
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const _app_reference = app_reference.trim().replace(/\/$/, "");
|
||||
|
||||
if (RE_SPACE_NAME.test(_app_reference)) {
|
||||
// app_reference is a HF space name
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://huggingface.co/api/spaces/${_app_reference}/${HOST_URL}`,
|
||||
{ headers }
|
||||
);
|
||||
|
||||
const _host = (await res.json()).host;
|
||||
|
||||
return {
|
||||
space_id: app_reference,
|
||||
...determine_protocol(_host)
|
||||
};
|
||||
} catch (e) {
|
||||
throw new Error(SPACE_METADATA_ERROR_MSG);
|
||||
}
|
||||
}
|
||||
|
||||
if (RE_SPACE_DOMAIN.test(_app_reference)) {
|
||||
// app_reference is a direct HF space domain
|
||||
const { ws_protocol, http_protocol, host } =
|
||||
determine_protocol(_app_reference);
|
||||
|
||||
return {
|
||||
space_id: host.split("/")[0].replace(".hf.space", ""),
|
||||
ws_protocol,
|
||||
http_protocol,
|
||||
host
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
space_id: false,
|
||||
...determine_protocol(_app_reference)
|
||||
};
|
||||
}
|
||||
|
||||
export const join_urls = (...urls: string[]): string => {
|
||||
try {
|
||||
return urls.reduce((base_url: string, part: string) => {
|
||||
base_url = base_url.replace(/\/+$/, "");
|
||||
part = part.replace(/^\/+/, "");
|
||||
return new URL(part, base_url + "/").toString();
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(INVALID_URL_MSG);
|
||||
}
|
||||
};
|
||||
|
||||
export function transform_api_info(
|
||||
api_info: ApiInfo<ApiData>,
|
||||
config: Config,
|
||||
api_map: Record<string, number>
|
||||
): ApiInfo<JsApiData> {
|
||||
const transformed_info: ApiInfo<JsApiData> = {
|
||||
named_endpoints: {},
|
||||
unnamed_endpoints: {}
|
||||
};
|
||||
|
||||
Object.keys(api_info).forEach((category) => {
|
||||
if (category === "named_endpoints" || category === "unnamed_endpoints") {
|
||||
transformed_info[category] = {};
|
||||
|
||||
Object.entries(api_info[category]).forEach(
|
||||
([endpoint, { parameters, returns }]) => {
|
||||
const dependencyIndex =
|
||||
config.dependencies.find(
|
||||
(dep) =>
|
||||
dep.api_name === endpoint ||
|
||||
dep.api_name === endpoint.replace("/", "")
|
||||
)?.id ||
|
||||
api_map[endpoint.replace("/", "")] ||
|
||||
-1;
|
||||
|
||||
const dependencyTypes =
|
||||
dependencyIndex !== -1
|
||||
? config.dependencies.find((dep) => dep.id == dependencyIndex)
|
||||
?.types
|
||||
: { generator: false, cancel: false };
|
||||
|
||||
if (
|
||||
dependencyIndex !== -1 &&
|
||||
config.dependencies.find((dep) => dep.id == dependencyIndex)?.inputs
|
||||
?.length !== parameters.length
|
||||
) {
|
||||
const components = config.dependencies
|
||||
.find((dep) => dep.id == dependencyIndex)!
|
||||
.inputs.map(
|
||||
(input) => config.components.find((c) => c.id === input)?.type
|
||||
);
|
||||
|
||||
try {
|
||||
components.forEach((comp, idx) => {
|
||||
if (comp === "state") {
|
||||
const new_param = {
|
||||
component: "state",
|
||||
example: null,
|
||||
parameter_default: null,
|
||||
parameter_has_default: true,
|
||||
parameter_name: null,
|
||||
hidden: true
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
parameters.splice(idx, 0, new_param);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
const transform_type = (
|
||||
data: ApiData,
|
||||
component: string,
|
||||
serializer: string,
|
||||
signature_type: "return" | "parameter"
|
||||
): JsApiData => ({
|
||||
...data,
|
||||
description: get_description(data?.type, serializer),
|
||||
type:
|
||||
get_type(data?.type, component, serializer, signature_type) || ""
|
||||
});
|
||||
|
||||
transformed_info[category][endpoint] = {
|
||||
parameters: parameters.map((p: ApiData) =>
|
||||
transform_type(p, p?.component, p?.serializer, "parameter")
|
||||
),
|
||||
returns: returns.map((r: ApiData) =>
|
||||
transform_type(r, r?.component, r?.serializer, "return")
|
||||
),
|
||||
type: dependencyTypes
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return transformed_info;
|
||||
}
|
||||
|
||||
export function get_type(
|
||||
type: { type: any; description: string },
|
||||
component: string,
|
||||
serializer: string,
|
||||
signature_type: "return" | "parameter"
|
||||
): string | undefined {
|
||||
if (component === "Api") return type.type;
|
||||
switch (type?.type) {
|
||||
case "string":
|
||||
return "string";
|
||||
case "boolean":
|
||||
return "boolean";
|
||||
case "number":
|
||||
return "number";
|
||||
}
|
||||
|
||||
if (
|
||||
serializer === "JSONSerializable" ||
|
||||
serializer === "StringSerializable"
|
||||
) {
|
||||
return "any";
|
||||
} else if (serializer === "ListStringSerializable") {
|
||||
return "string[]";
|
||||
} else if (component === "Image") {
|
||||
return signature_type === "parameter" ? "Blob | File | Buffer" : "string";
|
||||
} else if (serializer === "FileSerializable") {
|
||||
if (type?.type === "array") {
|
||||
return signature_type === "parameter"
|
||||
? "(Blob | File | Buffer)[]"
|
||||
: `{ name: string; data: string; size?: number; is_file?: boolean; orig_name?: string}[]`;
|
||||
}
|
||||
return signature_type === "parameter"
|
||||
? "Blob | File | Buffer"
|
||||
: `{ name: string; data: string; size?: number; is_file?: boolean; orig_name?: string}`;
|
||||
} else if (serializer === "GallerySerializable") {
|
||||
return signature_type === "parameter"
|
||||
? "[(Blob | File | Buffer), (string | null)][]"
|
||||
: `[{ name: string; data: string; size?: number; is_file?: boolean; orig_name?: string}, (string | null))][]`;
|
||||
}
|
||||
}
|
||||
|
||||
export function get_description(
|
||||
type: { type: any; description: string },
|
||||
serializer: string
|
||||
): string {
|
||||
if (serializer === "GallerySerializable") {
|
||||
return "array of [file, label] tuples";
|
||||
} else if (serializer === "ListStringSerializable") {
|
||||
return "array of strings";
|
||||
} else if (serializer === "FileSerializable") {
|
||||
return "array of files or single file";
|
||||
}
|
||||
return type?.description;
|
||||
}
|
||||
|
||||
export function handle_message(
|
||||
data: any,
|
||||
last_status: Status["stage"]
|
||||
): {
|
||||
type:
|
||||
| "hash"
|
||||
| "data"
|
||||
| "update"
|
||||
| "complete"
|
||||
| "generating"
|
||||
| "log"
|
||||
| "none"
|
||||
| "heartbeat"
|
||||
| "streaming"
|
||||
| "broken_connection"
|
||||
| "unexpected_error";
|
||||
data?: any;
|
||||
status?: Status;
|
||||
original_msg?: string;
|
||||
} {
|
||||
const queue = true;
|
||||
switch (data.msg) {
|
||||
case "send_data":
|
||||
return { type: "data" };
|
||||
case "send_hash":
|
||||
return { type: "hash" };
|
||||
case "queue_full":
|
||||
return {
|
||||
type: "update",
|
||||
status: {
|
||||
queue,
|
||||
message: QUEUE_FULL_MSG,
|
||||
stage: "error",
|
||||
code: data.code,
|
||||
success: data.success
|
||||
}
|
||||
};
|
||||
case "heartbeat":
|
||||
return {
|
||||
type: "heartbeat"
|
||||
};
|
||||
case "unexpected_error":
|
||||
return {
|
||||
type: "unexpected_error",
|
||||
status: {
|
||||
queue,
|
||||
message: data.message,
|
||||
session_not_found: data.session_not_found,
|
||||
stage: "error",
|
||||
success: false
|
||||
}
|
||||
};
|
||||
case "broken_connection":
|
||||
return {
|
||||
type: "broken_connection",
|
||||
status: {
|
||||
queue,
|
||||
message: data.message,
|
||||
stage: "error",
|
||||
success: false
|
||||
}
|
||||
};
|
||||
case "estimation":
|
||||
return {
|
||||
type: "update",
|
||||
status: {
|
||||
queue,
|
||||
stage: last_status || "pending",
|
||||
code: data.code,
|
||||
size: data.queue_size,
|
||||
position: data.rank,
|
||||
eta: data.rank_eta,
|
||||
success: data.success
|
||||
}
|
||||
};
|
||||
case "progress":
|
||||
return {
|
||||
type: "update",
|
||||
status: {
|
||||
queue,
|
||||
stage: "pending",
|
||||
code: data.code,
|
||||
progress_data: data.progress_data,
|
||||
success: data.success
|
||||
}
|
||||
};
|
||||
case "log":
|
||||
return { type: "log", data: data };
|
||||
case "process_generating":
|
||||
return {
|
||||
type: "generating",
|
||||
status: {
|
||||
queue,
|
||||
message: !data.success ? data.output.error : null,
|
||||
stage: data.success ? "generating" : "error",
|
||||
code: data.code,
|
||||
progress_data: data.progress_data,
|
||||
eta: data.average_duration,
|
||||
changed_state_ids: data.success
|
||||
? data.output.changed_state_ids
|
||||
: undefined
|
||||
},
|
||||
data: data.success ? data.output : null
|
||||
};
|
||||
case "process_streaming":
|
||||
return {
|
||||
type: "streaming",
|
||||
status: {
|
||||
queue,
|
||||
message: data.output.error,
|
||||
stage: "streaming",
|
||||
time_limit: data.time_limit,
|
||||
code: data.code,
|
||||
progress_data: data.progress_data,
|
||||
changed_state_ids: data.output.changed_state_ids,
|
||||
eta: data.eta
|
||||
},
|
||||
data: data.output
|
||||
};
|
||||
case "process_completed":
|
||||
if ("error" in data.output) {
|
||||
return {
|
||||
type: "update",
|
||||
status: {
|
||||
queue,
|
||||
title: (data.output.title as string | null) ?? "Error",
|
||||
message:
|
||||
(data.output.error as string | null) ?? "An error occurred",
|
||||
visible: data.output.visible as boolean,
|
||||
duration: data.output.duration as number,
|
||||
stage: "error",
|
||||
code: data.code,
|
||||
success: data.success
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: "complete",
|
||||
status: {
|
||||
queue,
|
||||
message: !data.success ? data.output.error : undefined,
|
||||
stage: data.success ? "complete" : "error",
|
||||
code: data.code,
|
||||
progress_data: data.progress_data,
|
||||
changed_state_ids: data.success
|
||||
? data.output.changed_state_ids
|
||||
: undefined,
|
||||
used_cache: data.used_cache,
|
||||
cache_duration: data.cache_duration,
|
||||
avg_time: data.avg_time
|
||||
},
|
||||
data: data.success ? data.output : null
|
||||
};
|
||||
|
||||
case "process_starts":
|
||||
return {
|
||||
type: "update",
|
||||
status: {
|
||||
queue,
|
||||
stage: "pending",
|
||||
code: data.code,
|
||||
size: data.rank,
|
||||
position: 0,
|
||||
success: data.success,
|
||||
eta: data.eta
|
||||
},
|
||||
original_msg: "process_starts"
|
||||
};
|
||||
}
|
||||
|
||||
return { type: "none", status: { stage: "error", queue } };
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the provided `data` to the parameters defined by the `/info` endpoint response.
|
||||
* This allows us to support both positional and keyword arguments passed to the client
|
||||
* and ensures that all parameters are either directly provided or have default values assigned.
|
||||
*
|
||||
* @param {unknown[] | Record<string, unknown>} data - The input data for the function,
|
||||
* which can be either an array of values for positional arguments or an object
|
||||
* with key-value pairs for keyword arguments.
|
||||
* @param {JsApiData[]} parameters - Array of parameter descriptions retrieved from the
|
||||
* `/info` endpoint.
|
||||
*
|
||||
* @returns {unknown[]} - Returns an array of resolved data where each element corresponds
|
||||
* to the expected parameter from the API. The `parameter_default` value is used where
|
||||
* a value is not provided for a parameter, and optional parameters without defaults are
|
||||
* set to `undefined`.
|
||||
*
|
||||
* @throws {Error} - Throws an error:
|
||||
* - If more arguments are provided than are defined in the parameters.
|
||||
* * - If no parameter value is provided for a required parameter and no default value is defined.
|
||||
* - If an argument is provided that does not match any defined parameter.
|
||||
*/
|
||||
|
||||
export const map_data_to_params = (
|
||||
data: unknown[] | Record<string, unknown> = [],
|
||||
endpoint_info: EndpointInfo<JsApiData | ApiData>
|
||||
): unknown[] => {
|
||||
// Workaround for the case where the endpoint_info is undefined
|
||||
// See https://github.com/gradio-app/gradio/pull/8820#issuecomment-2237381761
|
||||
const parameters = endpoint_info ? endpoint_info.parameters : [];
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
if (
|
||||
endpoint_info &&
|
||||
parameters.length > 0 &&
|
||||
data.length > parameters.length
|
||||
) {
|
||||
console.warn("Too many arguments provided for the endpoint.");
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
const resolved_data: unknown[] = [];
|
||||
const provided_keys = Object.keys(data);
|
||||
|
||||
parameters.forEach((param, index) => {
|
||||
if (data.hasOwnProperty(param.parameter_name)) {
|
||||
resolved_data[index] = data[param.parameter_name];
|
||||
} else if (param.parameter_has_default) {
|
||||
resolved_data[index] = param.parameter_default;
|
||||
} else {
|
||||
throw new Error(
|
||||
`No value provided for required parameter: ${param.parameter_name}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
provided_keys.forEach((key) => {
|
||||
if (!parameters.some((param) => param.parameter_name === key)) {
|
||||
throw new Error(
|
||||
`Parameter \`${key}\` is not a valid keyword argument. Please refer to the API for usage.`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
resolved_data.forEach((value, idx) => {
|
||||
if (value === undefined && !parameters[idx].parameter_has_default) {
|
||||
throw new Error(
|
||||
`No value provided for required parameter: ${parameters[idx].parameter_name}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return resolved_data;
|
||||
};
|
||||
@@ -0,0 +1,221 @@
|
||||
import {
|
||||
type ApiData,
|
||||
type BlobRef,
|
||||
type Config,
|
||||
type EndpointInfo,
|
||||
type JsApiData,
|
||||
type DataType,
|
||||
Command,
|
||||
type Dependency,
|
||||
type ComponentMeta
|
||||
} from "../types";
|
||||
import { FileData } from "../upload";
|
||||
|
||||
const is_node =
|
||||
typeof process !== "undefined" && process.versions && process.versions.node;
|
||||
|
||||
export function update_object(
|
||||
object: { [x: string]: any },
|
||||
newValue: any,
|
||||
stack: (string | number)[]
|
||||
): void {
|
||||
while (stack.length > 1) {
|
||||
const key = stack.shift();
|
||||
if (typeof key === "string" || typeof key === "number") {
|
||||
object = object[key];
|
||||
} else {
|
||||
throw new Error("Invalid key type");
|
||||
}
|
||||
}
|
||||
|
||||
const key = stack.shift();
|
||||
if (typeof key === "string" || typeof key === "number") {
|
||||
object[key] = newValue;
|
||||
} else {
|
||||
throw new Error("Invalid key type");
|
||||
}
|
||||
}
|
||||
|
||||
export async function walk_and_store_blobs(
|
||||
data: DataType,
|
||||
type: string | undefined = undefined,
|
||||
path: string[] = [],
|
||||
root = false,
|
||||
endpoint_info: EndpointInfo<ApiData | JsApiData> | undefined = undefined
|
||||
): Promise<BlobRef[]> {
|
||||
if (Array.isArray(data)) {
|
||||
let blob_refs: BlobRef[] = [];
|
||||
|
||||
await Promise.all(
|
||||
data.map(async (_, index) => {
|
||||
let new_path = path.slice();
|
||||
new_path.push(String(index));
|
||||
|
||||
const array_refs = await walk_and_store_blobs(
|
||||
data[index],
|
||||
root
|
||||
? endpoint_info?.parameters[index]?.component || undefined
|
||||
: type,
|
||||
new_path,
|
||||
false,
|
||||
endpoint_info
|
||||
);
|
||||
|
||||
blob_refs = blob_refs.concat(array_refs);
|
||||
})
|
||||
);
|
||||
|
||||
return blob_refs;
|
||||
} else if (
|
||||
(globalThis.Buffer && data instanceof globalThis.Buffer) ||
|
||||
data instanceof Blob
|
||||
) {
|
||||
return [
|
||||
{
|
||||
path: path,
|
||||
blob: new Blob([data as any]),
|
||||
type
|
||||
}
|
||||
];
|
||||
} else if (typeof data === "object" && data !== null) {
|
||||
let blob_refs: BlobRef[] = [];
|
||||
for (const key of Object.keys(data) as (keyof typeof data)[]) {
|
||||
const new_path = [...path, key];
|
||||
const value = data[key];
|
||||
|
||||
blob_refs = blob_refs.concat(
|
||||
await walk_and_store_blobs(
|
||||
value,
|
||||
undefined,
|
||||
new_path,
|
||||
false,
|
||||
endpoint_info
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return blob_refs;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export function skip_queue(id: number, config: Config): boolean {
|
||||
let fn_queue = config?.dependencies?.find((dep) => dep.id == id)?.queue;
|
||||
if (fn_queue != null) {
|
||||
return !fn_queue;
|
||||
}
|
||||
return !config.enable_queue;
|
||||
}
|
||||
|
||||
// todo: add jsdoc for this function
|
||||
|
||||
export function post_message<Res = any>(
|
||||
message: any,
|
||||
origin: string
|
||||
): Promise<Res> {
|
||||
return new Promise((res, _rej) => {
|
||||
const channel = new MessageChannel();
|
||||
channel.port1.onmessage = (({ data }) => {
|
||||
channel.port1.close();
|
||||
res(data);
|
||||
}) as (ev: MessageEvent<Res>) => void;
|
||||
window.parent.postMessage(message, origin, [channel.port2]);
|
||||
});
|
||||
}
|
||||
|
||||
export function handle_file(
|
||||
file_or_url: File | string | Blob | Buffer
|
||||
): FileData | Blob | Command {
|
||||
if (typeof file_or_url === "string") {
|
||||
if (
|
||||
file_or_url.startsWith("http://") ||
|
||||
file_or_url.startsWith("https://")
|
||||
) {
|
||||
return {
|
||||
path: file_or_url,
|
||||
url: file_or_url,
|
||||
orig_name: file_or_url.split("/").pop() ?? "unknown",
|
||||
meta: { _type: "gradio.FileData" }
|
||||
};
|
||||
}
|
||||
|
||||
if (is_node) {
|
||||
// Handle local file paths
|
||||
return new Command("upload_file", {
|
||||
path: file_or_url,
|
||||
name: file_or_url,
|
||||
orig_path: file_or_url
|
||||
});
|
||||
}
|
||||
} else if (typeof File !== "undefined" && file_or_url instanceof File) {
|
||||
return new Blob([file_or_url]);
|
||||
} else if (file_or_url instanceof Buffer) {
|
||||
return new Blob([file_or_url as any]);
|
||||
} else if (file_or_url instanceof Blob) {
|
||||
return file_or_url;
|
||||
}
|
||||
throw new Error(
|
||||
"Invalid input: must be a URL, File, Blob, or Buffer object."
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the payload by filtering out state inputs and returning an array of resolved payload values.
|
||||
* We send null values for state inputs to the server, but we don't want to include them in the resolved payload.
|
||||
*
|
||||
* @param resolved_payload - The resolved payload values received from the client or the server
|
||||
* @param dependency - The dependency object.
|
||||
* @param components - The array of component metadata.
|
||||
* @param with_null_state - Optional. Specifies whether to include null values for state inputs. Default is false.
|
||||
* @returns An array of resolved payload values, filtered based on the dependency and component metadata.
|
||||
*/
|
||||
export function handle_payload(
|
||||
resolved_payload: unknown[],
|
||||
dependency: Dependency,
|
||||
components: ComponentMeta[],
|
||||
type: "input" | "output",
|
||||
with_null_state = false
|
||||
): unknown[] {
|
||||
if (type === "input" && !with_null_state) {
|
||||
throw new Error("Invalid code path. Cannot skip state inputs for input.");
|
||||
}
|
||||
// data comes from the server with null state values so we skip
|
||||
if (type === "output" && with_null_state) {
|
||||
return resolved_payload;
|
||||
}
|
||||
|
||||
let updated_payload: unknown[] = [];
|
||||
let payload_index = 0;
|
||||
const deps = type === "input" ? dependency.inputs : dependency.outputs;
|
||||
for (let i = 0; i < deps.length; i++) {
|
||||
const input_id = deps[i];
|
||||
const component = components.find((c) => c.id === input_id);
|
||||
|
||||
if (component?.type === "state") {
|
||||
// input + with_null_state needs us to fill state with null values
|
||||
if (with_null_state) {
|
||||
if (resolved_payload.length === deps.length) {
|
||||
const value = resolved_payload[payload_index];
|
||||
updated_payload.push(value);
|
||||
payload_index++;
|
||||
} else {
|
||||
updated_payload.push(null);
|
||||
}
|
||||
} else {
|
||||
// this is output & !with_null_state, we skip state inputs
|
||||
// the server payload always comes with null state values so we move along the payload index
|
||||
payload_index++;
|
||||
continue;
|
||||
}
|
||||
// input & !with_null_state isn't a case we care about, server needs null
|
||||
continue;
|
||||
} else {
|
||||
const value = resolved_payload[payload_index];
|
||||
updated_payload.push(value);
|
||||
payload_index++;
|
||||
}
|
||||
}
|
||||
|
||||
return updated_payload;
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
import type { Config } from "../types";
|
||||
import {
|
||||
CONFIG_ERROR_MSG,
|
||||
CONFIG_URL,
|
||||
INVALID_CREDENTIALS_MSG,
|
||||
LOGIN_URL,
|
||||
MISSING_CREDENTIALS_MSG,
|
||||
SPACE_METADATA_ERROR_MSG,
|
||||
UNAUTHORIZED_MSG
|
||||
} from "../constants";
|
||||
import { Client } from "..";
|
||||
import { join_urls, process_endpoint } from "./api_info";
|
||||
|
||||
/**
|
||||
* This function is used to resolve the URL for making requests when the app has a root path.
|
||||
* The root path could be a path suffix like "/app" which is appended to the end of the base URL. Or
|
||||
* it could be a full URL like "https://abidlabs-test-client-replica--gqf2x.hf.space" which is used when hosting
|
||||
* Gradio apps on Hugging Face Spaces.
|
||||
* @param {string} base_url The base URL at which the Gradio server is hosted
|
||||
* @param {string} root_path The root path, which could be a path suffix (e.g. mounted in FastAPI app) or a full URL (e.g. hosted on Hugging Face Spaces)
|
||||
* @param {boolean} prioritize_base Whether to prioritize the base URL over the root path. This is used when both the base path and root paths are full URLs. For example, for fetching files the root path should be prioritized, but for making requests, the base URL should be prioritized.
|
||||
* @returns {string} the resolved URL
|
||||
*/
|
||||
export function resolve_root(
|
||||
base_url: string,
|
||||
root_path: string,
|
||||
prioritize_base: boolean
|
||||
): string {
|
||||
if (root_path.startsWith("http://") || root_path.startsWith("https://")) {
|
||||
return prioritize_base ? base_url : root_path;
|
||||
}
|
||||
return base_url + root_path;
|
||||
}
|
||||
|
||||
export async function get_jwt(
|
||||
space: string,
|
||||
token: `hf_${string}`,
|
||||
cookies?: string | null
|
||||
): Promise<string | false> {
|
||||
try {
|
||||
const r = await fetch(`https://huggingface.co/api/spaces/${space}/jwt`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
...(cookies ? { Cookie: cookies } : {})
|
||||
}
|
||||
});
|
||||
|
||||
const jwt = (await r.json()).token;
|
||||
|
||||
return jwt || false;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function map_names_to_ids(
|
||||
fns: Config["dependencies"]
|
||||
): Record<string, number> {
|
||||
let apis: Record<string, number> = {};
|
||||
|
||||
fns.forEach(({ api_name, id }) => {
|
||||
if (api_name) apis[api_name] = id;
|
||||
});
|
||||
return apis;
|
||||
}
|
||||
|
||||
export async function resolve_config(
|
||||
this: Client,
|
||||
endpoint: string
|
||||
): Promise<Config | undefined> {
|
||||
const headers: Record<string, string> = this.options.token
|
||||
? { Authorization: `Bearer ${this.options.token}` }
|
||||
: {};
|
||||
|
||||
if (
|
||||
typeof window !== "undefined" &&
|
||||
window.gradio_config &&
|
||||
location.origin !== "http://localhost:9876"
|
||||
) {
|
||||
if (window.gradio_config.current_page) {
|
||||
endpoint = endpoint.substring(0, endpoint.lastIndexOf("/"));
|
||||
}
|
||||
if (
|
||||
window.gradio_config.dev_mode ||
|
||||
(typeof window !== "undefined" && window?.BUILD_MODE === "dev")
|
||||
) {
|
||||
let config_url = join_urls(
|
||||
endpoint,
|
||||
this.deep_link
|
||||
? CONFIG_URL + "?deep_link=" + this.deep_link
|
||||
: CONFIG_URL
|
||||
);
|
||||
const response = await this.fetch(config_url, {
|
||||
headers,
|
||||
credentials: this.options.credentials ?? "same-origin"
|
||||
});
|
||||
const config = await handleConfigResponse(response, !!this.options.auth);
|
||||
config.root = endpoint || config.root;
|
||||
// @ts-ignore
|
||||
window.gradio_config = {
|
||||
...config,
|
||||
current_page: window.gradio_config.current_page
|
||||
};
|
||||
}
|
||||
// @ts-ignore
|
||||
return { ...window.gradio_config } as Config;
|
||||
} else if (endpoint) {
|
||||
let config_url = join_urls(
|
||||
endpoint,
|
||||
this.deep_link ? CONFIG_URL + "?deep_link=" + this.deep_link : CONFIG_URL
|
||||
);
|
||||
|
||||
const response = await this.fetch(config_url, {
|
||||
headers,
|
||||
credentials: this.options.credentials ?? "same-origin"
|
||||
});
|
||||
|
||||
const config = await handleConfigResponse(response, !!this.options.auth);
|
||||
// Preserve the backend-provided root if available (it contains the correct public URL)
|
||||
// Only fall back to endpoint if the backend didn't provide a root
|
||||
if (!config.root) {
|
||||
config.root = endpoint;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
throw new Error(CONFIG_ERROR_MSG);
|
||||
}
|
||||
|
||||
async function handleConfigResponse(
|
||||
response: Response,
|
||||
authorized: boolean
|
||||
): Promise<Config> {
|
||||
if (response?.status === 401 && !authorized) {
|
||||
const error_data = await response.json();
|
||||
const auth_message = error_data?.detail?.auth_message;
|
||||
throw new Error(auth_message || MISSING_CREDENTIALS_MSG);
|
||||
} else if (response?.status === 401 && authorized) {
|
||||
throw new Error(INVALID_CREDENTIALS_MSG);
|
||||
}
|
||||
|
||||
if (response?.status === 200) {
|
||||
let config = await response.json();
|
||||
config.dependencies?.forEach((dep: any, i: number) => {
|
||||
if (dep.id === undefined) {
|
||||
dep.id = i;
|
||||
}
|
||||
});
|
||||
return config;
|
||||
} else if (response?.status === 401) {
|
||||
throw new Error(UNAUTHORIZED_MSG);
|
||||
}
|
||||
|
||||
throw new Error(CONFIG_ERROR_MSG);
|
||||
}
|
||||
|
||||
export async function resolve_cookies(this: Client): Promise<void> {
|
||||
const { http_protocol, host } = await process_endpoint(
|
||||
this.app_reference,
|
||||
this.options.token
|
||||
);
|
||||
|
||||
try {
|
||||
if (this.options.auth) {
|
||||
const cookie_header = await get_cookie_header(
|
||||
http_protocol,
|
||||
host,
|
||||
this.options.auth,
|
||||
this.fetch,
|
||||
this.options.token,
|
||||
this.options.credentials
|
||||
);
|
||||
|
||||
if (cookie_header) this.set_cookies(cookie_header);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
throw Error((e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
// separating this from client-bound resolve_cookies so that it can be used in duplicate
|
||||
export async function get_cookie_header(
|
||||
http_protocol: string,
|
||||
host: string,
|
||||
auth: [string, string],
|
||||
_fetch: typeof fetch,
|
||||
token?: `hf_${string}`,
|
||||
credentials?: RequestCredentials
|
||||
): Promise<string | null> {
|
||||
const formData = new FormData();
|
||||
formData.append("username", auth?.[0]);
|
||||
formData.append("password", auth?.[1]);
|
||||
|
||||
let headers: { Authorization?: string } = {};
|
||||
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const res = await _fetch(`${http_protocol}//${host}/${LOGIN_URL}`, {
|
||||
headers,
|
||||
method: "POST",
|
||||
body: formData,
|
||||
credentials: credentials ?? "same-origin"
|
||||
});
|
||||
|
||||
if (res.status === 200) {
|
||||
return res.headers.get("set-cookie");
|
||||
} else if (res.status === 401) {
|
||||
throw new Error(INVALID_CREDENTIALS_MSG);
|
||||
} else {
|
||||
throw new Error(SPACE_METADATA_ERROR_MSG);
|
||||
}
|
||||
}
|
||||
|
||||
export function determine_protocol(endpoint: string): {
|
||||
ws_protocol: "ws" | "wss";
|
||||
http_protocol: "http:" | "https:";
|
||||
host: string;
|
||||
} {
|
||||
if (endpoint.startsWith("http")) {
|
||||
const { protocol, host, pathname } = new URL(endpoint);
|
||||
|
||||
return {
|
||||
ws_protocol: protocol === "https:" ? "wss" : "ws",
|
||||
http_protocol: protocol as "http:" | "https:",
|
||||
host: host + (pathname !== "/" ? pathname : "")
|
||||
};
|
||||
}
|
||||
|
||||
// default to secure if no protocol is provided
|
||||
|
||||
return {
|
||||
ws_protocol: "wss",
|
||||
http_protocol: "https:",
|
||||
host: new URL(endpoint).host
|
||||
};
|
||||
}
|
||||
|
||||
export const parse_and_set_cookies = (cookie_header: string): string[] => {
|
||||
let cookies: string[] = [];
|
||||
const parts = cookie_header.split(/,(?=\s*[^\s=;]+=[^\s=;]+)/);
|
||||
parts.forEach((cookie) => {
|
||||
const [cookie_name, cookie_value] = cookie.split(";")[0].split("=");
|
||||
if (cookie_name && cookie_value) {
|
||||
cookies.push(`${cookie_name.trim()}=${cookie_value.trim()}`);
|
||||
}
|
||||
});
|
||||
return cookies;
|
||||
};
|
||||
@@ -0,0 +1,252 @@
|
||||
import {
|
||||
RUNTIME_URL,
|
||||
SLEEPTIME_URL,
|
||||
SPACE_STATUS_ERROR_MSG
|
||||
} from "../constants";
|
||||
import { RE_SPACE_NAME } from "./api_info";
|
||||
import type { SpaceStatusCallback } from "../types";
|
||||
|
||||
export async function check_space_status(
|
||||
id: string,
|
||||
type: "subdomain" | "space_name",
|
||||
status_callback: SpaceStatusCallback
|
||||
): Promise<void> {
|
||||
let endpoint =
|
||||
type === "subdomain"
|
||||
? `https://huggingface.co/api/spaces/by-subdomain/${id}`
|
||||
: `https://huggingface.co/api/spaces/${id}`;
|
||||
let response;
|
||||
let _status;
|
||||
try {
|
||||
response = await fetch(endpoint);
|
||||
_status = response.status;
|
||||
if (_status !== 200) {
|
||||
throw new Error();
|
||||
}
|
||||
response = await response.json();
|
||||
} catch (e) {
|
||||
status_callback({
|
||||
status: "error",
|
||||
load_status: "error",
|
||||
message: SPACE_STATUS_ERROR_MSG,
|
||||
detail: "NOT_FOUND"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response || _status !== 200) return;
|
||||
const {
|
||||
runtime: { stage },
|
||||
id: space_name
|
||||
} = response;
|
||||
|
||||
switch (stage) {
|
||||
case "STOPPED":
|
||||
case "SLEEPING":
|
||||
status_callback({
|
||||
status: "sleeping",
|
||||
load_status: "pending",
|
||||
message: "Space is asleep. Waking it up...",
|
||||
detail: stage
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
check_space_status(id, type, status_callback);
|
||||
}, 1000); // poll for status
|
||||
break;
|
||||
case "PAUSED":
|
||||
status_callback({
|
||||
status: "paused",
|
||||
load_status: "error",
|
||||
message:
|
||||
"This space has been paused by the author. If you would like to try this demo, consider duplicating the space.",
|
||||
detail: stage,
|
||||
discussions_enabled: await discussions_enabled(space_name)
|
||||
});
|
||||
break;
|
||||
case "RUNNING":
|
||||
case "RUNNING_BUILDING":
|
||||
status_callback({
|
||||
status: "running",
|
||||
load_status: "complete",
|
||||
message: "Space is running.",
|
||||
detail: stage
|
||||
});
|
||||
break;
|
||||
case "BUILDING":
|
||||
status_callback({
|
||||
status: "building",
|
||||
load_status: "pending",
|
||||
message: "Space is building...",
|
||||
detail: stage
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
check_space_status(id, type, status_callback);
|
||||
}, 1000);
|
||||
break;
|
||||
case "APP_STARTING":
|
||||
status_callback({
|
||||
status: "starting",
|
||||
load_status: "pending",
|
||||
message: "Space is starting...",
|
||||
detail: stage
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
check_space_status(id, type, status_callback);
|
||||
}, 1000);
|
||||
break;
|
||||
default:
|
||||
status_callback({
|
||||
status: "space_error",
|
||||
load_status: "error",
|
||||
message: "This space is experiencing an issue.",
|
||||
detail: stage,
|
||||
discussions_enabled: await discussions_enabled(space_name)
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export const check_and_wake_space = async (
|
||||
space_id: string,
|
||||
status_callback: SpaceStatusCallback
|
||||
): Promise<void> => {
|
||||
let retries = 0;
|
||||
const max_retries = 12;
|
||||
const check_interval = 5000;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
check_space_status(
|
||||
space_id,
|
||||
RE_SPACE_NAME.test(space_id) ? "space_name" : "subdomain",
|
||||
(status) => {
|
||||
status_callback(status);
|
||||
|
||||
if (status.status === "running") {
|
||||
resolve();
|
||||
} else if (
|
||||
status.status === "error" ||
|
||||
status.status === "paused" ||
|
||||
status.status === "space_error"
|
||||
) {
|
||||
resolve();
|
||||
} else if (
|
||||
status.status === "sleeping" ||
|
||||
status.status === "building"
|
||||
) {
|
||||
if (retries < max_retries) {
|
||||
retries++;
|
||||
setTimeout(() => {
|
||||
check_and_wake_space(space_id, status_callback).then(resolve);
|
||||
}, check_interval);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const RE_DISABLED_DISCUSSION =
|
||||
/^(?=[^]*\b[dD]iscussions{0,1}\b)(?=[^]*\b[dD]isabled\b)[^]*$/;
|
||||
export async function discussions_enabled(space_id: string): Promise<boolean> {
|
||||
try {
|
||||
const r = await fetch(
|
||||
`https://huggingface.co/api/spaces/${space_id}/discussions`,
|
||||
{
|
||||
method: "HEAD"
|
||||
}
|
||||
);
|
||||
|
||||
const error = r.headers.get("x-error-message");
|
||||
|
||||
if (!r.ok || (error && RE_DISABLED_DISCUSSION.test(error))) return false;
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function get_space_hardware(
|
||||
space_id: string,
|
||||
token?: `hf_${string}` | undefined
|
||||
): Promise<(typeof hardware_types)[number]> {
|
||||
const headers: { Authorization?: string } = {};
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://huggingface.co/api/spaces/${space_id}/${RUNTIME_URL}`,
|
||||
{ headers }
|
||||
);
|
||||
|
||||
if (res.status !== 200)
|
||||
throw new Error("Space hardware could not be obtained.");
|
||||
|
||||
const { hardware } = await res.json();
|
||||
|
||||
return hardware.current;
|
||||
} catch (e: any) {
|
||||
throw new Error(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
export async function set_space_timeout(
|
||||
space_id: string,
|
||||
timeout: number,
|
||||
token?: `hf_${string}`
|
||||
): Promise<any> {
|
||||
const headers: { Authorization?: string } = {};
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const body: {
|
||||
seconds?: number;
|
||||
} = {
|
||||
seconds: timeout
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://huggingface.co/api/spaces/${space_id}/${SLEEPTIME_URL}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...headers },
|
||||
body: JSON.stringify(body)
|
||||
}
|
||||
);
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error(
|
||||
"Could not set sleep timeout on duplicated Space. Please visit *ADD HF LINK TO SETTINGS* to set a timeout manually to reduce billing charges."
|
||||
);
|
||||
}
|
||||
|
||||
const response = await res.json();
|
||||
return response;
|
||||
} catch (e: any) {
|
||||
throw new Error(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
export const hardware_types = [
|
||||
"cpu-basic",
|
||||
"cpu-upgrade",
|
||||
"cpu-xl",
|
||||
"t4-small",
|
||||
"t4-medium",
|
||||
"a10g-small",
|
||||
"a10g-large",
|
||||
"a10g-largex2",
|
||||
"a10g-largex4",
|
||||
"a100-large",
|
||||
"zero-a10g",
|
||||
"h100",
|
||||
"h100x8"
|
||||
] as const;
|
||||
@@ -0,0 +1,40 @@
|
||||
const ZEROGPU_HEADERS_MESSAGE = "supports-zerogpu-headers";
|
||||
|
||||
let zerogpu_handshake_initialized = false;
|
||||
|
||||
function supports_browser_handshake(): boolean {
|
||||
return (
|
||||
typeof window !== "undefined" &&
|
||||
typeof document !== "undefined" &&
|
||||
typeof window.addEventListener === "function"
|
||||
);
|
||||
}
|
||||
|
||||
export function get_zerogpu_origin(hostname: string): string | null {
|
||||
if (hostname.includes(".dev.")) {
|
||||
return `https://moon-${hostname.split(".")[1]}.dev.spaces.huggingface.tech`;
|
||||
}
|
||||
if (hostname.endsWith(".hf.space")) {
|
||||
return "https://huggingface.co";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function initialize_zerogpu_handshake(): void {
|
||||
if (!supports_browser_handshake() || zerogpu_handshake_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener("message", (event) => {
|
||||
if (event.data === ZEROGPU_HEADERS_MESSAGE) {
|
||||
window.supports_zerogpu_headers = true;
|
||||
}
|
||||
});
|
||||
|
||||
zerogpu_handshake_initialized = true;
|
||||
|
||||
const origin = get_zerogpu_origin(window.location.hostname);
|
||||
if (origin && window.parent !== window) {
|
||||
window.parent.postMessage(ZEROGPU_HEADERS_MESSAGE, origin);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user